diff --git a/AutoDarkModeApp/App.xaml.cs b/AutoDarkModeApp/App.xaml.cs index 623aef720..d44d09d6c 100644 --- a/AutoDarkModeApp/App.xaml.cs +++ b/AutoDarkModeApp/App.xaml.cs @@ -25,90 +25,106 @@ public partial class App : Application // https://docs.microsoft.com/dotnet/core/extensions/logging public IHost Host { get; } + /// + /// Retrieves a registered service of the specified type from the application's dependency injection container. + /// + /// Use this method to access services configured in the application's dependency injection + /// container. Ensure that the service type is registered in the ConfigureServices method of App.xaml.cs before + /// calling this method. + /// The type of service to retrieve. Must be a reference type and registered in the application's service + /// collection. + /// An instance of the requested service type . + /// Thrown if the requested service type is not registered in the application's service + /// collection. public static T GetService() where T : class { if ((Current as App)!.Host.Services.GetService(typeof(T)) is not T service) { - throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); + throw new InvalidOperationException($"{typeof(T)} needs to be registered in ConfigureServices within App.xaml.cs."); } return service; } + public static MainWindow MainWindow { get; private set; } = null!; + + public App() + { + CheckAppMutex(); + InitializeComponent(); + + Host = Microsoft.Extensions.Hosting.Host + .CreateDefaultBuilder() + .UseContentRoot(AppContext.BaseDirectory) + .ConfigureServices((context, services) => + { + // Services + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + // Window + // NOTE: The MainWindow is registered as a singleton because we only need one instance. + services.AddSingleton(); + + // Views and ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + + // Configuration + services.Configure(context.Configuration.GetSection(nameof(LocalSettingsOptions))); + } + ) + .Build(); + + UnhandledException += App_UnhandledException; + } + public static void CheckAppMutex() { if (!Mutex.WaitOne(TimeSpan.FromMilliseconds(50), false) && !Debugger.IsAttached) { - List processes = [.. Process.GetProcessesByName("AutoDarkModeApp")]; + var processes = Process.GetProcessesByName("AutoDarkModeApp").Where(p => p.Id != Environment.ProcessId).ToList(); if (processes.Count > 0) { Helpers.WindowHelper.BringProcessToFront(processes[0]); - Environment.Exit(-1); + App.Current.Exit(); } } } - public static Window MainWindow { get; set; } = Window.Current; - - public App() - { - CheckAppMutex(); - - InitializeComponent(); - - Host = Microsoft.Extensions.Hosting.Host. - CreateDefaultBuilder(). - UseContentRoot(AppContext.BaseDirectory). - ConfigureServices((context, services) => - { - // Services - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - - // Views and ViewModels - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - - // Configuration - services.Configure(context.Configuration.GetSection(nameof(LocalSettingsOptions))); - }). - Build(); - - UnhandledException += App_UnhandledException; - } - private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) { // TODO: Log and handle exceptions as appropriate. @@ -124,26 +140,26 @@ protected async override void OnLaunched(LaunchActivatedEventArgs args) if (arguments.Length > 1) { new PipeClient().SendMessageAndGetReply(arguments[1]); - Environment.Exit(-1); + App.Current.Exit(); + return; } // Set App and Svc language Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = await LanguageHelper.GetDefaultLanguageAsync(); - var builder = AdmConfigBuilder.Instance(); - builder.Load(); - try + await Task.Run(() => { - builder.Config.Tunable.UICulture = LanguageHelper.SelectedLanguageCode; // save before activation SVC (think of first-launch scenario) + var builder = AdmConfigBuilder.Instance(); + builder.Load(); + builder.Config.Tunable.UICulture = LanguageHelper.SelectedLanguageCode; // For Svc and other services that need to know the UI culture builder.Save(); - } - catch (Exception) - { - // We can't show a dialog here - //await App.GetService().ShowErrorMessage(ex, App.MainWindow.Content.XamlRoot, "Startup", "Failed to force-set Svc language"); - } + }); + + // NOTE: Here we use the DI container to get the MainWindow and set it as a static property, which not only conforms to the standard, but also facilitates other places to access the MainWindow. + MainWindow = GetService(); + MainWindow.Closed += async (s, e) => await GetService().CloseAsync(); - MainWindow = new MainWindow(); + await GetService().ActivateAsync(args); - await App.GetService().ActivateAsync(args); + // NOTE: The MainWindow must be activated (i.e. made visible) in the ActivationService, not here in App.xaml.cs, because there are navigation events and adjustment of window position and size. } } diff --git a/AutoDarkModeApp/Contracts/Services/ICloseService.cs b/AutoDarkModeApp/Contracts/Services/ICloseService.cs new file mode 100644 index 000000000..28deb2ae3 --- /dev/null +++ b/AutoDarkModeApp/Contracts/Services/ICloseService.cs @@ -0,0 +1,6 @@ +namespace AutoDarkModeApp.Contracts.Services; + +public interface ICloseService +{ + Task CloseAsync(); +} diff --git a/AutoDarkModeApp/Helpers/LanguageHelper.cs b/AutoDarkModeApp/Helpers/LanguageHelper.cs index 63dd336eb..5cc0956ab 100644 --- a/AutoDarkModeApp/Helpers/LanguageHelper.cs +++ b/AutoDarkModeApp/Helpers/LanguageHelper.cs @@ -9,7 +9,6 @@ public static class LanguageHelper { public static string SelectedLanguageCode { get; set; } = "en-US"; // equal to - private static readonly ILocalSettingsService _localSettingsService = App.GetService()!; public static readonly string[] SupportedCultures = [ // Left-to-Right (LTR) languages @@ -23,7 +22,8 @@ public static class LanguageHelper public static async Task GetDefaultLanguageAsync() { - var language = await _localSettingsService.ReadSettingAsync("SelectedLanguageCode"); + var localSettingsService = App.GetService(); + var language = await localSettingsService.ReadSettingAsync("SelectedLanguageCode"); if (!string.IsNullOrEmpty(language) && SupportedCultures.Contains(language)) { SelectedLanguageCode = language; @@ -56,7 +56,7 @@ public static async Task GetDefaultLanguageAsync() } // else keep the default "en-US" } - await _localSettingsService.SaveSettingAsync("SelectedLanguageCode", SelectedLanguageCode); + await localSettingsService.SaveSettingAsync("SelectedLanguageCode", SelectedLanguageCode); } return SelectedLanguageCode; } diff --git a/AutoDarkModeApp/MainWindow.xaml.cs b/AutoDarkModeApp/MainWindow.xaml.cs index 77b25808e..8acb6a73c 100644 --- a/AutoDarkModeApp/MainWindow.xaml.cs +++ b/AutoDarkModeApp/MainWindow.xaml.cs @@ -12,14 +12,16 @@ public sealed partial class MainWindow : Window { private readonly INavigationService _navigationService; - public MainWindow() + [DllImport("user32.dll")] + private static extern uint GetDpiForWindow(IntPtr hWnd); + + public MainWindow(INavigationService navigationService) { - _navigationService = App.GetService(); + _navigationService = navigationService; InitializeComponent(); - // TODO: Set the title bar icon by updating /Assets/WindowIcon.ico. - // A custom title bar is required for full window theme and Mica support. - // https://docs.microsoft.com/windows/apps/develop/title-bar?tabs=winui3#full-customization + Title = Debugger.IsAttached ? "Auto Dark Mode Debug" : "Auto Dark Mode"; + ExtendsContentIntoTitleBar = true; SetTitleBar(TitleBar); TitleBar.Subtitle = Debugger.IsAttached ? "Debug" : ""; @@ -27,32 +29,24 @@ public MainWindow() ApplySystemThemeToCaptionButtons(); - AppWindow.SetIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico")); - AppWindow.SetTaskbarIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico")); - AppWindow.SetTitleBarIcon(Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico")); + var iconPath = Path.Combine(AppContext.BaseDirectory, "Assets/AutoDarkModeIcon.ico"); + AppWindow.SetIcon(iconPath); + AppWindow.SetTaskbarIcon(iconPath); + AppWindow.SetTitleBarIcon(iconPath); - Title = Debugger.IsAttached ? "Auto Dark Mode Debug" : "Auto Dark Mode"; + IntPtr hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this); + uint dpi = GetDpiForWindow(hwnd); + double scaleFactor = dpi / 96.0; - // TODO: No one knows what the correct way to use it is. Waiting for official examples. - [DllImport("user32.dll")] - static extern uint GetDpiForWindow([In] IntPtr hwnd); - IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); - var dpiForWindow = GetDpiForWindow(hWnd); - double scaleFactor = dpiForWindow / 96.0; if (AppWindow.Presenter is OverlappedPresenter presenter) { - presenter.PreferredMinimumWidth = (int)(870 * scaleFactor); + presenter.PreferredMinimumWidth = (int)(860 * scaleFactor); presenter.PreferredMinimumHeight = (int)(600 * scaleFactor); - presenter.PreferredMaximumHeight = 10000; - presenter.PreferredMaximumWidth = 10000; } _navigationService.Frame = NavigationFrame; _navigationService.InitializeNavigationView(NavigationViewControl); - _navigationService.InitializeBreadcrumbBar(BreadcrumBarControl); - - Closed += MainWindow_Closed; } private void NavViewTitleBar_BackRequested(Microsoft.UI.Xaml.Controls.TitleBar sender, object args) @@ -74,36 +68,4 @@ private void ApplySystemThemeToCaptionButtons() AppWindow.TitleBar.ButtonHoverBackgroundColor = backgroundHoverColor; } - private async void MainWindow_Closed(object sender, WindowEventArgs args) - { - var presenter = AppWindow.Presenter as OverlappedPresenter; - var position = AppWindow.Position; - var size = AppWindow.Size; - var localSettings = App.GetService(); - await Task.Run(async () => - { - if (presenter != null) - { - await localSettings.SaveSettingAsync("WindowState", (int)presenter.State); - - if (presenter.State == OverlappedPresenterState.Restored) - { - await localSettings.SaveSettingAsync("X", position.X); - await localSettings.SaveSettingAsync("Y", position.Y); - await localSettings.SaveSettingAsync("Width", size.Width); - await localSettings.SaveSettingAsync("Height", size.Height); - } - } - - //TODO: MapLocationFinder will make WinUI app hang on exit, more information on https://github.com/microsoft/microsoft-ui-xaml/issues/10229 - try - { - Process.GetCurrentProcess().Kill(); - } - catch (Exception ex) - { - Debug.WriteLine(ex.Message); - } - }); - } } diff --git a/AutoDarkModeApp/Services/CloseService.cs b/AutoDarkModeApp/Services/CloseService.cs new file mode 100644 index 000000000..10af46ba6 --- /dev/null +++ b/AutoDarkModeApp/Services/CloseService.cs @@ -0,0 +1,26 @@ +using AutoDarkModeApp.Contracts.Services; +using Microsoft.UI.Windowing; + +namespace AutoDarkModeApp.Services; + +public class CloseService(ILocalSettingsService localSettingsService) : ICloseService +{ + public async Task CloseAsync() + { + if (App.MainWindow.AppWindow.Presenter is OverlappedPresenter presenter) + { + var position = App.MainWindow.AppWindow.Position; + var size = App.MainWindow.AppWindow.Size; + + await localSettingsService.SaveSettingAsync("WindowState", (int)presenter.State); + + if (presenter.State == OverlappedPresenterState.Restored) + { + await localSettingsService.SaveSettingAsync("X", position.X); + await localSettingsService.SaveSettingAsync("Y", position.Y); + await localSettingsService.SaveSettingAsync("Width", size.Width); + await localSettingsService.SaveSettingAsync("Height", size.Height); + } + } + } +}