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.
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 windowLinuxApplication.Lifecycle.cs(1) -- callsShutdown()Hosting/LinuxProgramHost.cs(1) -- callsInitialize()Hosting/LinuxMauiAppBuilderExtensions.cs(1) -- DI registration (already wrapping Instance)Handlers/GtkWebViewPlatformView.cs(1) -- readsHostWindowHandlers/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) -- readsCurrentTheme, subscribes toThemeChangedServices/AppInfoService.cs(1) -- readsCurrentThemeforRequestedTheme
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) -- callsShapeTextWithFallbackRendering/TextRenderingHelper.cs(1) -- callsShapeTextWithFallback
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_inputMethodServicefieldViews/SkiaEntry.cs(1) -- assigns to_inputMethodServicefield
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) -- accessesPrimaryMonitor
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 registrationLinuxApplication.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.
Recommended DI Registrations
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)
- DeviceInfoService -- zero non-registration references; no dependencies
- ConnectivityService -- zero non-registration references; implements
IDisposable - LinuxDispatcherProvider -- one non-registration reference in
LinuxApplication.Lifecycle.cs - DeviceDisplayService -- zero non-registration references; depends on
MonitorService - MonitorService -- one non-registration reference (in
DeviceDisplayService); migrate together with #4 - AppInfoService -- zero non-registration references; depends on
SystemThemeService - AccessibilityServiceFactory -- one non-registration reference; factory pattern
Medium (requires updating view/rendering classes to accept injected dependencies)
- SystemThemeService -- two non-registration references in
LinuxApplication.Lifecycle.cs; also depended on byAppInfoService - FontFallbackManager -- three non-registration references in view/rendering code
- InputMethodServiceFactory -- two non-registration references in view code (
SkiaEditor,SkiaEntry)
Hard (requires refactoring the application startup pipeline)
- 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