Ein gängiges Muster, das verwendet werden kann, um die Modularität in der Codebasis einer Anwendung mithilfe des MVVM-Musters zu erhöhen, besteht darin, eine Form von Inversion der Kontrolle zu verwenden. Eine der gängigsten Lösungen ist die Verwendung von Dependency Injection. Dabei wird eine Reihe von Diensten erstellt, die in Backend-Klassen injiziert werden (d. h. sie werden als Parameter an die ViewModel-Konstruktoren übergeben). Dadurch ist Code, der diese Dienste verwendet, nicht von den Implementierungsdetails dieser Dienste abhängig, und außerdem lassen sich die konkreten Implementierungen dieser Dienste leicht austauschen. Dieses Muster erleichtert auch die Bereitstellung plattformspezifischer Features für Back-End-Code, indem sie durch einen Dienst abstrahiert werden, der bei Bedarf eingefügt wird.

Das MVVM-Toolkit bietet keine integrierten APIs, um die Verwendung dieses Musters zu erleichtern, da es bereits spezialisierte Bibliotheken eigens für diesen Zweck gibt, wie z. B. das Paket Microsoft.Extensions.DependencyInjection, das einen umfassenden und leistungsstarken Satz an DI-APIs bereitstellt und als leicht einzurichtendes und zu verwendendes IServiceProvider fungiert. Die folgende Anleitung bezieht sich auf diese Bibliothek und zeigt anhand einer Reihe von Beispielen, wie sie mithilfe des MVVM-Musters in Anwendungen integriert werden kann.

Plattform-APIs:Ioc

Konfigurieren und Auflösen von Diensten

Der erste Schritt besteht darin, eine IServiceProvider-Instanz zu deklarieren und alle erforderlichen Dienste zu initialisieren, normalerweise beim Start. Beispielsweise kann unter UWP ein ähnliches Setup auch für andere Frameworks verwendet werden:

public sealed partial class App : Application
{
    public App()
    {
        Services = ConfigureServices();

        this.InitializeComponent();
    }

    /// <summary>
    /// Gets the current <see cref="App"/> instance in use
    /// </summary>
    public new static App Current => (App)Application.Current;

    /// <summary>
    /// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
    /// </summary>
    public IServiceProvider Services { get; }

    /// <summary>
    /// Configures the services for the application.
    /// </summary>
    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<IFilesService, FilesService>();
        services.AddSingleton<ISettingsService, SettingsService>();
        services.AddSingleton<IClipboardService, ClipboardService>();
        services.AddSingleton<IShareService, ShareService>();
        services.AddSingleton<IEmailService, EmailService>();

        return services.BuildServiceProvider();
    }
}

Hier wird die Services-Eigenschaft beim Start initialisiert, und alle Anwendungsdienste und Viewmodels werden registriert. Es gibt auch eine neue Current-Eigenschaft, die verwendet werden kann, um auf einfache Weise über andere Ansichten in der Anwendung auf die Services-Eigenschaft zuzugreifen. Beispiel:

IFilesService filesService = App.Current.Services.GetService<IFilesService>();

// Use the files service here...

Der wichtigste Aspekt hier ist, dass jeder Dienst möglicherweise sehr gut plattformspezifische APIs verwendet, aber da diese alle über die Schnittstelle abstrahiert sind, die unser Code verwendet, müssen wir uns nicht um sie kümmern, wenn wir nur eine Instanz auflösen und sie zum Ausführen von Vorgängen verwenden.

Konstruktorinjektion

Ein leistungsfähiges Feature, das verfügbar ist, ist „Konstruktoreinfügung“, was bedeutet, dass der DI-Dienstanbieter indirekte Abhängigkeiten zwischen registrierten Diensten automatisch auflösen kann, wenn Instanzen des angeforderten Typs erstellt werden. Betrachten Sie den folgenden Dienst:

public class FileLogger : IFileLogger
{
    private readonly IFilesService FileService;
    private readonly IConsoleService ConsoleService;

    public FileLogger(
        IFilesService fileService,
        IConsoleService consoleService)
    {
        FileService = fileService;
        ConsoleService = consoleService;
    }

    // Methods for the IFileLogger interface here...
}

Hier haben wir einen FileLogger-Typ, der die IFileLogger-Schnittstelle implementiert und die Instanzen IFilesService und IConsoleService benötigt. Die Konstruktorinjektion bedeutet, dass der DI-Dienstanbieter automatisch alle erforderlichen Dienste sammelt, z. B.:

/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFilesService, FilesService>();
    services.AddSingleton<IConsoleService, ConsoleService>();
    services.AddSingleton<IFileLogger, FileLogger>();

    return services.BuildServiceProvider();
}

// Retrieve a logger service with constructor injection
IFileLogger fileLogger = App.Current.Services.GetService<IFileLogger>();

Der DI-Dienstanbieter überprüft automatisch, ob alle erforderlichen Dienste registriert sind, und ruft sie dann ab und ruft den Konstruktor für den registrierten IFileLogger konkreten Typ auf, um die Instanz zurückzugeben.

Was ist mit Viewmodels?

Ein Serviceanbieter hat „Service“ in seinem Namen, kann aber tatsächlich verwendet werden, um Instanzen beliebiger Klassen aufzulösen, einschließlich ViewModels! Die oben erläuterten Konzepte gelten nach wie vor, einschließlich der Konstruktorinjektion. Stellen Sie sich vor, wir hätten einen ContactsViewModel-Typ, der über seinen Konstruktor eine IContactsService- und eine IPhoneService-Instanz verwendet. Wir könnten eine ConfigureServices-Methode wie folgt haben:

/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    // Services
    services.AddSingleton<IContactsService, ContactsService>();
    services.AddSingleton<IPhoneService, PhoneService>();

    // Viewmodels
    services.AddTransient<ContactsViewModel>();

    return services.BuildServiceProvider();
}

Und dann würden wir in unserem ContactsView den Datenkontext wie folgt zuweisen:

public ContactsView()
{
    this.InitializeComponent();
    this.DataContext = App.Current.Services.GetService<ContactsViewModel>();
}

Weitere Dokumente

Weitere Informationen über Microsoft.Extensions.DependencyInjection finden Sie hier.

Beispiele

  • Sehen Sie sich die Beispiel-App (für mehrere Benutzeroberflächen-Frameworks) an, um das MVVM-Toolkit in Aktion zu sehen.
  • Weitere Beispiele finden Sie auch in den Komponententests.