Files
logikonline 6b45f28b4e feat(interop): add safe handle wrappers and configuration options
Implement SafeHandle wrappers for native resources (SafeGtkWidgetHandle, SafeGObjectHandle, SafeX11DisplayHandle, SafeX11CursorHandle, SafeDlopenHandle) to prevent memory leaks. Move gesture and rendering configuration from static properties to LinuxApplicationOptions for better testability and DI compatibility. Add THREADING.md and DI-MIGRATION.md documentation. Include LayoutIntegrationTests for Measure/Arrange pipeline and SkiaViewTheoryTests with parameterized test cases using [Theory] attributes.
2026-03-06 23:40:51 -05:00

16 KiB

Singleton-to-DI Migration Plan

This document catalogs all singleton service classes in the maui-linux project that use a static Instance property pattern instead of proper dependency injection, and provides a migration plan for each.


Identified Singleton Services

1. GtkHostService

Property Value
File Services/GtkHostService.cs
Pattern _instance ??= new GtkHostService() (null-coalescing assignment)
Interface None (concrete class)
Static references 8 across 6 files
Init/Cleanup Initialize(title, width, height) must be called before use; Shutdown() must be called on exit
DI Registration AddSingleton<GtkHostService>
Difficulty Hard

Files referencing GtkHostService.Instance:

  • LinuxApplication.cs (2) -- creates/configures host window
  • LinuxApplication.Lifecycle.cs (1) -- calls Shutdown()
  • Hosting/LinuxProgramHost.cs (1) -- calls Initialize()
  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration (already wrapping Instance)
  • Handlers/GtkWebViewPlatformView.cs (1) -- reads HostWindow
  • Handlers/GtkWebViewHandler.cs (2) -- reads host service

Notes: This service is initialized very early in the startup path (LinuxProgramHost) before the DI container is fully built, and is also accessed by LinuxApplication which itself is constructed before DI resolution. Migration requires refactoring the startup pipeline so that GtkHostService is constructed by the container and then explicitly initialized via a hosted-service or startup hook.


2. SystemThemeService

Property Value
File Services/SystemThemeService.cs
Pattern Double-checked locking with lock (_lock)
Interface None (concrete class)
Static references 3 across 2 files (excluding its own definition)
Init/Cleanup Constructor auto-detects theme and starts a FileSystemWatcher + polling Timer
DI Registration AddSingleton<SystemThemeService>
Difficulty Medium

Files referencing SystemThemeService.Instance:

  • LinuxApplication.Lifecycle.cs (2) -- reads CurrentTheme, subscribes to ThemeChanged
  • Services/AppInfoService.cs (1) -- reads CurrentTheme for RequestedTheme

Notes: Already registered in DI (TryAddSingleton<SystemThemeService>()), but the registration does not use the static Instance -- it lets the container construct it. However, call sites still use SystemThemeService.Instance directly instead of resolving from the container. AppInfoService itself is a singleton and would need the SystemThemeService injected via constructor.


3. FontFallbackManager

Property Value
File Services/FontFallbackManager.cs
Pattern Double-checked locking with lock (_lock)
Interface None (concrete class)
Static references 4 across 3 files (excluding its own definition)
Init/Cleanup Constructor pre-caches top 10 fallback font typefaces
DI Registration AddSingleton<FontFallbackManager>
Difficulty Medium

Files referencing FontFallbackManager.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration (wrapping Instance)
  • Views/SkiaLabel.cs (2) -- calls ShapeTextWithFallback
  • Rendering/TextRenderingHelper.cs (1) -- calls ShapeTextWithFallback

Notes: The view and rendering classes currently access the static Instance directly rather than accepting it via constructor injection. These classes would need to receive FontFallbackManager through the handler or view constructor.


4. InputMethodServiceFactory

Property Value
File Services/InputMethodServiceFactory.cs
Pattern Static factory class with double-checked locking
Interface Returns IInputMethodService
Static references 3 across 3 files (excluding its own definition)
Init/Cleanup Factory auto-detects IBus/Fcitx5/XIM; Reset() calls Shutdown() on existing instance
DI Registration AddSingleton<IInputMethodService> (via factory delegate)
Difficulty Medium

Files referencing InputMethodServiceFactory.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration (wrapping Instance)
  • Views/SkiaEditor.cs (1) -- assigns to _inputMethodService field
  • Views/SkiaEntry.cs (1) -- assigns to _inputMethodService field

Notes: The factory pattern can be replaced with a DI factory registration: AddSingleton<IInputMethodService>(sp => InputMethodServiceFactory.CreateService()). View classes should receive IInputMethodService via constructor injection.


5. AccessibilityServiceFactory

Property Value
File Services/AccessibilityServiceFactory.cs
Pattern Static factory class with double-checked locking
Interface Returns IAccessibilityService
Static references 2 across 2 files (excluding its own definition)
Init/Cleanup Factory creates AtSpi2AccessibilityService and calls Initialize(); Reset() calls Shutdown()
DI Registration AddSingleton<IAccessibilityService> (via factory delegate)
Difficulty Easy

Files referencing AccessibilityServiceFactory.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration (wrapping Instance)
  • Views/SkiaView.Accessibility.cs (1) -- accesses the service

Notes: Similar to InputMethodServiceFactory. Replace with a factory delegate in DI registration. Only one non-registration call site to update.


6. ConnectivityService

Property Value
File Services/ConnectivityService.cs
Pattern Lazy<ConnectivityService>
Interface IConnectivity, IDisposable
Static references 1 across 1 file (excluding its own definition)
Init/Cleanup Constructor subscribes to NetworkChange events; Dispose() unsubscribes
DI Registration AddSingleton<IConnectivity>
Difficulty Easy

Files referencing ConnectivityService.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration

Notes: Already only referenced via the DI registration line. Migration is trivial: change to AddSingleton<IConnectivity, ConnectivityService>(). The constructor is already public.


7. DeviceDisplayService

Property Value
File Services/DeviceDisplayService.cs
Pattern Lazy<DeviceDisplayService>
Interface IDeviceDisplay
Static references 1 across 1 file (excluding its own definition)
Init/Cleanup Constructor calls RefreshDisplayInfo(); internally references MonitorService.Instance
DI Registration AddSingleton<IDeviceDisplay>
Difficulty Easy

Files referencing DeviceDisplayService.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration

Notes: Only referenced at the DI registration point. The dependency on MonitorService.Instance inside RefreshDisplayInfo() means MonitorService should be migrated first (or simultaneously) and injected via constructor.


8. AppInfoService

Property Value
File Services/AppInfoService.cs
Pattern Lazy<AppInfoService>
Interface IAppInfo
Static references 1 across 1 file (excluding its own definition)
Init/Cleanup Constructor reads assembly metadata; RequestedTheme accesses SystemThemeService.Instance
DI Registration AddSingleton<IAppInfo>
Difficulty Easy

Files referencing AppInfoService.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration

Notes: Only referenced at the DI registration point. Has an internal dependency on SystemThemeService.Instance that should be replaced with constructor injection of SystemThemeService.


9. DeviceInfoService

Property Value
File Services/DeviceInfoService.cs
Pattern Lazy<DeviceInfoService>
Interface IDeviceInfo
Static references 1 across 1 file (excluding its own definition)
Init/Cleanup Constructor reads /sys/class/dmi/id/ files
DI Registration AddSingleton<IDeviceInfo>
Difficulty Easy

Files referencing DeviceInfoService.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration

Notes: No dependencies on other singletons. Simplest migration candidate.


10. MonitorService

Property Value
File Services/MonitorService.cs
Pattern Double-checked locking with lock (_lock)
Interface IDisposable
Static references 2 across 2 files (excluding its own definition)
Init/Cleanup Lazy initialization via EnsureInitialized(); opens X11 display; Dispose() closes display
DI Registration AddSingleton<MonitorService>
Difficulty Easy

Files referencing MonitorService.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration (wrapping Instance)
  • Services/DeviceDisplayService.cs (1) -- accesses PrimaryMonitor

Notes: Referenced by DeviceDisplayService internally. Both should be migrated together, with MonitorService injected into DeviceDisplayService via constructor.


11. LinuxDispatcherProvider

Property Value
File Dispatching/LinuxDispatcherProvider.cs
Pattern _instance ?? (_instance = new ...) (not thread-safe)
Interface IDispatcherProvider
Static references 2 across 2 files (excluding its own definition)
Init/Cleanup None
DI Registration AddSingleton<IDispatcherProvider>
Difficulty Easy

Files referencing LinuxDispatcherProvider.Instance:

  • Hosting/LinuxMauiAppBuilderExtensions.cs (1) -- DI registration
  • LinuxApplication.Lifecycle.cs (1) -- used during lifecycle setup

Notes: Minimal state, no initialization. Trivial to migrate.


Services That SHOULD Remain Static

DiagnosticLog

Property Value
File Services/DiagnosticLog.cs
Pattern Pure static class (not a singleton Instance pattern)
References 518 occurrences across 74 files

Rationale for keeping static: DiagnosticLog is used pervasively throughout the entire codebase, including inside constructors of the singleton services themselves, during startup before the DI container is built, in native interop code, in static factory methods, and in exception handlers. Converting it to an injected dependency would require threading a logger parameter through every class in the project and would create circular dependency issues (services log during their own construction). It is a cross-cutting concern that is appropriately static.


Replace the current registration block in Hosting/LinuxMauiAppBuilderExtensions.cs with:

// Dispatcher
builder.Services.TryAddSingleton<IDispatcherProvider, LinuxDispatcherProvider>();

// Device services (no inter-dependencies)
builder.Services.TryAddSingleton<IDeviceInfo, DeviceInfoService>();
builder.Services.TryAddSingleton<IConnectivity, ConnectivityService>();

// Theme and monitor services
builder.Services.TryAddSingleton<SystemThemeService>();
builder.Services.TryAddSingleton<MonitorService>();

// Services with dependencies on theme/monitor
builder.Services.TryAddSingleton<IAppInfo, AppInfoService>();          // depends on SystemThemeService
builder.Services.TryAddSingleton<IDeviceDisplay, DeviceDisplayService>(); // depends on MonitorService

// Factory-created services
builder.Services.TryAddSingleton<IAccessibilityService>(sp =>
{
    try
    {
        var service = new AtSpi2AccessibilityService();
        service.Initialize();
        return service;
    }
    catch
    {
        return new NullAccessibilityService();
    }
});

builder.Services.TryAddSingleton<IInputMethodService>(sp =>
    InputMethodServiceFactory.CreateService());

// Infrastructure services
builder.Services.TryAddSingleton<FontFallbackManager>();
builder.Services.TryAddSingleton<GtkHostService>();

Migration Priority by Difficulty

Easy (change registration + remove static Instance; few or no call-site changes)

  1. DeviceInfoService -- zero non-registration references; no dependencies
  2. ConnectivityService -- zero non-registration references; implements IDisposable
  3. LinuxDispatcherProvider -- one non-registration reference in LinuxApplication.Lifecycle.cs
  4. DeviceDisplayService -- zero non-registration references; depends on MonitorService
  5. MonitorService -- one non-registration reference (in DeviceDisplayService); migrate together with #4
  6. AppInfoService -- zero non-registration references; depends on SystemThemeService
  7. AccessibilityServiceFactory -- one non-registration reference; factory pattern

Medium (requires updating view/rendering classes to accept injected dependencies)

  1. SystemThemeService -- two non-registration references in LinuxApplication.Lifecycle.cs; also depended on by AppInfoService
  2. FontFallbackManager -- three non-registration references in view/rendering code
  3. InputMethodServiceFactory -- two non-registration references in view code (SkiaEditor, SkiaEntry)

Hard (requires refactoring the application startup pipeline)

  1. GtkHostService -- seven non-registration references across application core, startup host, and handler code; initialized before DI container is built

Migration Example: DeviceInfoService (Before/After)

Before

Services/DeviceInfoService.cs:

public class DeviceInfoService : IDeviceInfo
{
    private static readonly Lazy<DeviceInfoService> _instance =
        new Lazy<DeviceInfoService>(() => new DeviceInfoService());

    public static DeviceInfoService Instance => _instance.Value;

    public DeviceInfoService()
    {
        LoadDeviceInfo();
    }
    // ...
}

Hosting/LinuxMauiAppBuilderExtensions.cs:

builder.Services.TryAddSingleton<IDeviceInfo>(DeviceInfoService.Instance);

After

Services/DeviceInfoService.cs:

public class DeviceInfoService : IDeviceInfo
{
    // Remove: private static readonly Lazy<DeviceInfoService> _instance = ...
    // Remove: public static DeviceInfoService Instance => ...

    public DeviceInfoService()
    {
        LoadDeviceInfo();
    }
    // ... (rest unchanged)
}

Hosting/LinuxMauiAppBuilderExtensions.cs:

builder.Services.TryAddSingleton<IDeviceInfo, DeviceInfoService>();

No other files reference DeviceInfoService.Instance, so no further changes are needed. The DI container will construct the instance lazily on first resolution and manage its lifetime.


Dependency Graph for Migration Order

DeviceInfoService          (no deps)         -- migrate first
ConnectivityService        (no deps)
LinuxDispatcherProvider    (no deps)
MonitorService             (no deps)
  \--> DeviceDisplayService  (depends on MonitorService)
SystemThemeService         (no deps)
  \--> AppInfoService        (depends on SystemThemeService)
FontFallbackManager        (no deps, but used by views)
InputMethodServiceFactory  (no deps, but used by views)
AccessibilityServiceFactory (no deps)
GtkHostService             (no deps, but used before DI is built) -- migrate last