Dependency Injection v .NET MAUI: životní cykly služeb, registrace ViewModelů a správné vzory

Dependency injection v .NET MAUI staví na Microsoft.Extensions.DependencyInjection a celou konfiguraci soustředí do MauiProgram.cs. Zjistěte, kdy volit Singleton, Transient nebo Scoped, jak registrovat stránky a ViewModely, jak fungují Keyed services a jak se vyhnout captive dependency nebo Service Locator antipatternům.

Dependency Injection v .NET MAUI: životní cykly a vzory
On this page
  1. Konfigurace kontejneru v MauiProgram.cs
  2. Životní cykly: Singleton, Transient a Scoped v praxi
  3. Registrace stránek a ViewModelů
  4. Pokročilé vzory: továrny, podmíněná registrace a Keyed services
  5. Časté chyby a jak se jim vyhnout

Dependency injection v .NET MAUI staví na rozhraní Microsoft.Extensions.DependencyInjection a celou konfiguraci soustředí do jediného místa – metody CreateMauiApp() v souboru MauiProgram.cs. Framework tento kontejner spravuje automaticky: při navigaci Shell vyvolá konstruktory stránek a vyplní závislosti za vás. Klíčovým rozhodnutím je výběr životního cyklu pro každou registrovanou třídu – špatná volba se projeví úniky paměti nebo nechtěně sdíleným stavem mezi obrazovkami.

Tento průvodce vysvětluje, jak kontejner nakonfigurovat, jaký životní cyklus volit pro ViewModely, stránky a sdílené služby, a jaké vzory použít v situacích, kdy konstruktorová injektáž nestačí.

Konfigurace kontejneru v MauiProgram.cs

.NET MAUI inicializuje celou aplikaci v metodě CreateMauiApp() souboru MauiProgram.cs. Objekt MauiAppBuilder zpřístupňuje vlastnost Services – instanci IServiceCollection, do níž zaregistrujete závislosti ještě před spuštěním aplikace.

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            });

        // Infrastrukturní služby
        builder.Services.AddSingleton<ISettingsService, SettingsService>();
        builder.Services.AddSingleton<IDatabaseService, DatabaseService>();
        builder.Services.AddTransient<IApiClient, ApiClient>();

        // Stránky a ViewModely
        builder.Services.AddTransient<MainPage>();
        builder.Services.AddTransient<MainViewModel>();

        return builder.Build();
    }
}

Po volání builder.Build() kontejner uzamkne registrace – nové závislosti po tomto okamžiku přidat nelze. Framework pak předává IServiceProvider celé aplikaci automaticky: při navigaci Shell zavolá konstruktor cílové stránky a vyplní jeho parametry z kontejneru.

Registrace lze pro přehlednost rozdělit do extension metod na IServiceCollection:

public static IServiceCollection AddApplicationServices(
    this IServiceCollection services)
{
    services.AddSingleton<ISettingsService, SettingsService>();
    services.AddSingleton<IDatabaseService, DatabaseService>();
    return services;
}

public static IServiceCollection AddViewModels(
    this IServiceCollection services)
{
    services.AddTransient<MainViewModel>();
    services.AddTransient<MainPage>();
    return services;
}

// V MauiProgram.cs:
builder.Services
    .AddApplicationServices()
    .AddViewModels();

Tento vzor zachovává MauiProgram.cs přehledný i ve velkých aplikacích s desítkami registrací.

Životní cykly: Singleton, Transient a Scoped v praxi

Singleton

Kontejner vytvoří přesně jednu instanci a vrací ji při každém vyžádání po celou dobu běhu aplikace. Singleton je správná volba pro:

  • datové úložiště sdílené mezi obrazovkami (SQLite wrapper, in-memory cache)
  • konfiguraci a nastavení (ISettingsService, IPreferencesService)
  • autentizační stav udržující přihlašovací token
  • event bus nebo WeakReferenceMessenger z CommunityToolkit.Mvvm

Transient

Každé vyžádání ze ServiceProvider vrátí novou instanci. Transient je vhodný pro:

  • ViewModely navigačních stránek – každá navigace dostane čistý stav bez dat z předchozího zobrazení
  • objekty s explicitní dobou života, které záměrně nesdílíte
  • libovolnou třídu, jejíž sdílení by způsobilo souboje o stav mezi obrazovkami

Scoped

V ASP.NET Core odpovídá Scoped jeden HTTP požadavek. V .NET MAUI žádná taková přirozená hranice neexistuje: AddScoped bez explicitního vytvoření IServiceScope se chová identicky jako AddTransient – nová instance při každém resolve z root kontejneru.

Pokud potřebujete sdílený stav v rámci jednoho průvodce nebo registračního flow, vytvořte scope ručně:

public class WizardCoordinator : IDisposable
{
    private readonly IServiceScope _scope;

    public WizardCoordinator(IServiceProvider provider)
    {
        _scope = provider.CreateScope();
    }

    public T GetService<T>() where T : notnull
        => _scope.ServiceProvider.GetRequiredService<T>();

    public void Dispose() => _scope.Dispose();
}

Po dokončení průvodce zavoláte Dispose() a všechny Scoped závislosti z tohoto scope se uvolní. Registrace WizardCoordinator jako Transient zajistí, že každý nový průvodce dostane vlastní scope.

Registrace stránek a ViewModelů

Aby Shell mohl vytvořit stránku přes DI, musí být stránka zaregistrovaná v kontejneru. Stránky registrujte jako Transient, pokud chcete, aby každá navigace na danou stránku dostala čerstvý ViewModel:

builder.Services.AddTransient<ProductListPage>();
builder.Services.AddTransient<ProductListViewModel>();

builder.Services.AddTransient<ProductDetailPage>();
builder.Services.AddTransient<ProductDetailViewModel>();

Stránka přijme ViewModel v konstruktoru a nastaví BindingContext:

public partial class ProductListPage : ContentPage
{
    public ProductListPage(ProductListViewModel viewModel)
    {
        InitializeComponent();
        BindingContext = viewModel;
    }
}

ViewModel přijme datové služby v konstruktoru – bez přímé závislosti na Page:

public partial class ProductListViewModel : ObservableObject
{
    private readonly IProductService _productService;

    public ProductListViewModel(IProductService productService)
    {
        _productService = productService;
    }

    [RelayCommand]
    private async Task LoadProductsAsync()
    {
        // načtení dat...
    }
}

Registrace stránky a ViewModelu jako Transient zajistí, že Shell při každé navigaci na tuto stránku vytvoří nové instance obou tříd. Při přechodu zpět Shell stránku uvolní, a tím se uvolní i ViewModel.

Kdy registrovat stránku jako Singleton?

Stránky zaregistrované jako Singleton Shell vytvoří jednou a znovu použije při každé návratové navigaci. To může zrychlit opětovné zobrazení hlavní obrazovky, ale ViewModel musí sám ošetřit aktualizaci dat při příchodu – typicky v OnAppearing nebo přes WeakReferenceMessenger. Pro většinu případů je Transient bezpečnější volba; Singleton registraci stránky používejte vědomě a s plným vědomím o sdíleném stavu.

Podrobnosti o navigaci v Shell najdete v článku Navigace v .NET MAUI Shell, který popisuje routing, předávání dat a deep linking.

Pokročilé vzory: továrny, podmíněná registrace a Keyed services

Továrny pro krátkožijící závislosti

Singleton, který potřebuje vytvářet nové instance závislostí za běhu, by neměl mít přímou Transient závislost. Místo toho zaregistrujte delegát továrny jako Singleton – ten si ponechá odkaz na IServiceProvider a při každém volání vytvoří čerstvou Transient instanci:

// Registrace
builder.Services.AddTransient<IReportGenerator, PdfReportGenerator>();
builder.Services.AddSingleton<Func<IReportGenerator>>(
    sp => () => sp.GetRequiredService<IReportGenerator>());

// Spotřeba v Singletonu
public class ReportOrchestrator
{
    private readonly Func<IReportGenerator> _factory;

    public ReportOrchestrator(Func<IReportGenerator> factory)
    {
        _factory = factory;
    }

    public void Generate()
    {
        var generator = _factory();
        generator.Generate();
    }
}

Podmíněná registrace podle platformy

Různé platformy mohou vyžadovat různé implementace rozhraní. Podmíněnou registraci lze provést v MauiProgram.cs pomocí preprocesoru:

#if ANDROID
builder.Services.AddSingleton<INotificationService, AndroidNotificationService>();
#elif IOS
builder.Services.AddSingleton<INotificationService, iOSNotificationService>();
#endif

Nebo přes runtime podmínku s DeviceInfo, pokud nechcete duplikovat konfiguraci pro každý cílový rámec:

builder.Services.AddSingleton<INotificationService>(sp =>
    DeviceInfo.Platform == DevicePlatform.Android
        ? new AndroidNotificationService()
        : new iOSNotificationService());

Keyed services

Novější verze .NET přinesly Keyed services – možnost registrovat více implementací téhož rozhraní pod různými řetězcovými klíči. To řeší situaci, kdy potřebujete lokální i cloudové úložiště vedle sebe:

builder.Services.AddKeyedSingleton<IStorageProvider, LocalStorageProvider>("local");
builder.Services.AddKeyedSingleton<IStorageProvider, CloudStorageProvider>("cloud");

// Spotřeba přes atribut
public class SyncService(
    [FromKeyedServices("local")] IStorageProvider localStorage,
    [FromKeyedServices("cloud")] IStorageProvider cloudStorage)
{
    // ...
}

Keyed services jsou užitečné například pro offline/online strategie nebo pro různé backendy datového úložiště. Před použitím ověřte v poznámkách k vydání vaší verze .NET a .NET MAUI, zda je tato funkce podporována.

Časté chyby a jak se jim vyhnout

Captive dependency

Nastane, když Singleton drží přímou referenci na Transient závislost. Transient instance pak uvízne v Singletonu a sdílí se po celou dobu běhu aplikace – výsledkem je sdílený stav tam, kde ho nechcete. Příklad problematické registrace:

// ŠPATNĚ: Singleton drží Transient závislost
builder.Services.AddSingleton<IOrderService, OrderService>();
builder.Services.AddTransient<ICartState, CartState>();

public class OrderService
{
    private readonly ICartState _cart;

    public OrderService(ICartState cart) => _cart = cart;
}

Stav košíku je nyní sdílen po celou dobu aplikace. Řešením je injektovat továrnu (Func<ICartState>), přepracovat zodpovědnost, nebo přesunout stav košíku do Singletonu, pokud to dává smysl.

Service Locator antipattern

Přímé volání IServiceProvider.GetService<T>() uvnitř tříd skryje závislosti a ztíží testování. Konstruktorová injektáž je vždy preferována; IServiceProvider používejte jen ve třídách, kde DI rámec sám konstruktor nevolá (custom handlery, platform kód generovaný systémem). Správnou konstruktorovou injektáž ViewModelů ukazuje článek MVVM s CommunityToolkit.Mvvm.

IDisposable Transient bez scope

Pokud registrujete Transient službu implementující IDisposable a nevytvoříte explicitní scope, root kontejner drží reference na všechny vytvořené instance a neuvolní je do konce aplikace. Řešení:

  • Vytvořte explicitní IServiceScope a po použití ho dispose.
  • Zvažte, zda Transient opravdu potřebuje implementovat IDisposable, nebo zda lze lifetime resource přesunout jinam.

Kruhové závislosti

Circular dependency A → B → A způsobí výjimku při prvním resolve. Nejčastějším řešením je refaktorovat sdílenou zodpovědnost do třetí třídy nebo přerušit cyklus lazy inicializací. Pokud circular dependency vznikla kvůli komunikaci mezi ViewModely, zvažte místo toho WeakReferenceMessenger z CommunityToolkit.Mvvm.

Nejčastější dotazy

Musím registrovat každou stránku v DI kontejneru?

Pouze stránky s závislostmi v konstruktoru musí být zaregistrované. Stránky bez konstruktorových závislostí Shell dokáže vytvořit sám, ale pro konzistenci většina projektů registruje všechny stránky a ViewModely v MauiProgram.cs.

Jaký je praktický rozdíl mezi Singleton a statickou třídou?

Singleton v DI kontejneru implementuje rozhraní, takže ho lze v testech nahradit fake implementací. Statická třída je pevně svázaná s každým konsumentem a nelze ji snadno nahradit ani mockovat. Preferujte Singleton, kdykoli budete třídu testovat nebo potřebujete v budoucnu vyměnit implementaci.

Jak testovat ViewModel, který závisí na DI?

Vytvořte v testu instanci ViewModelu přímo s fiktivní (mock nebo stub) implementací závislostí – konstruktorová injektáž to umožňuje bez jakéhokoli DI kontejneru. Podrobnosti o testování aplikace najdete v článku o testování .NET MAUI aplikací.

Je bezpečné injektovat IServiceProvider přímo do ViewModelu?

Fungovat to bude, ale skrývá to závislosti a testování je obtížnější. Preferujte konkrétní závislosti v konstruktoru. IServiceProvider injektujte jen do koordinátorů a továrních tříd, které explicitně vytvářejí jiné objekty – nikoliv do řadových ViewModelů.

Article changelog (1)
  • — Content revised