Architectuurpatronen voor WinUI 3-bureaublad-apps

In dit artikel wordt beschreven hoe u bewezen architectuurpatronen toepast op WinUI 3-desktop-apps die zijn gebouwd met de Windows App SDK. U leert hoe u afhankelijkheidsinjectie kunt instellen, configuratie kunt beheren en code kunt structureren voor enterprise-Line-of-Business (LOB)-scenario's.

Prerequisites

  • Windows App SDK 1,5 of hoger
  • .NET 8 of hoger
  • Visual Studio 2022 versie 17.10 of later met de workloads .NET-desktopontwikkeling en Windows-applicatieontwikkeling

Afhankelijkheidsinjectie

WinUI 3-desktop-apps bevatten geen ingebouwde afhankelijkheidsinjectiecontainer (DI), zoals ASP.NET Core wel, maar u kunt er een toevoegen met hetzelfde Microsoft.Extensions.DependencyInjection NuGet-pakket. DI maakt uw code testbaar, losjes gekoppeld en eenvoudiger te onderhouden.

Een DI-container instellen

Installeer het NuGet-pakket:

dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting

Configureer de host en services in uw App.xaml.cs:

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.UI.Xaml;

public partial class App : Application
{
    public IHost Host { get; }

    public static T GetService<T>() where T : class
    {
        if ((App.Current as App)!.Host.Services.GetService(typeof(T)) is not T service)
        {
            throw new ArgumentException(
                $"{typeof(T)} needs to be registered in ConfigureServices.");
        }
        return service;
    }

    public App()
    {
        InitializeComponent();

        Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
            .UseContentRoot(AppContext.BaseDirectory)
            .ConfigureServices((context, services) =>
            {
                // Services
                services.AddSingleton<INavigationService, NavigationService>();
                services.AddSingleton<IDataService, DataService>();
                services.AddTransient<IDialogService, DialogService>();

                // ViewModels
                services.AddTransient<MainViewModel>();
                services.AddTransient<SettingsViewModel>();

                // Views
                services.AddTransient<MainPage>();
                services.AddTransient<SettingsPage>();
            })
            .Build();
    }
}

Opmerking

Als u vergeet een service te registreren, genereert de GetService<T>() bovenstaande helper een ArgumentException at runtime met de naam van het ontbrekende type. Voer de app uit en navigeer tijdens de ontwikkeling naar elke pagina om te controleren of alle registraties juist zijn.

Afhankelijkheden in ViewModels injecteren

Als de container is geconfigureerd, ontvangen uw ViewModels afhankelijkheden via constructorinjectie:

using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

public partial class MainViewModel : ObservableObject
{
    private readonly IDataService _dataService;
    private readonly INavigationService _navigationService;

    public MainViewModel(IDataService dataService, INavigationService navigationService)
    {
        _dataService = dataService;
        _navigationService = navigationService;
    }

    [ObservableProperty]
    private string _statusMessage = string.Empty;

    [RelayCommand]
    private async Task LoadDataAsync()
    {
        StatusMessage = "Loading...";
        var items = await _dataService.GetItemsAsync();
        StatusMessage = $"Loaded {items.Count} items";
    }
}

Levensduur van de service

Kies de juiste levensduur bij het registreren van services:

Levensduur Methode Te gebruiken voor
Singleton AddSingleton<T>() Navigatie, appbrede toestand, caches
Afgebakend AddScoped<T>() Contexten per venster of per dialoogvenster
Transient AddTransient<T>() ViewModels, statusloze services

Tip

Registreer ViewModels als tijdelijk , zodat elke navigatie een nieuw exemplaar maakt. Registreer services die een appbrede status hebben als Singleton.

Configuratiebeheer

Gebruik Microsoft.Extensions.Configuration dit voor het beheren van app-instellingen in bureaublad-apps, hetzelfde patroon dat wordt gebruikt in ASP.NET Core.

Configuratieondersteuning toevoegen

Installeer de vereiste pakketten:

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options

Maak een appsettings.json bestand in de hoofdmap van uw project. Klik in Solution Explorer met de rechtermuisknop op het bestand, selecteer Eigenschappen en stel Kopiëren naar uitvoermap in op Kopiëren indien nieuwer. U kunt ook <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> toevoegen aan de vermelding van het bestand in uw .csproj.

{
  "AppSettings": {
    "ApiBaseUrl": "https://api.contoso.com/v2",
    "MaxRetryCount": 3,
    "EnableTelemetry": true
  },
  "Logging": {
    "LogLevel": "Information"
  }
}

Bind de configuratie in uw DI-installatie:

.ConfigureServices((context, services) =>
{
    // Bind settings to a strongly-typed class
    services.Configure<AppSettings>(
        context.Configuration.GetSection("AppSettings"));

    // Inject IOptions<AppSettings> into services
    services.AddSingleton<IApiClient, ApiClient>();
})

Configuratie gebruiken in diensten

using Microsoft.Extensions.Options;

public class ApiClient : IApiClient
{
    private readonly AppSettings _settings;
    private readonly HttpClient _httpClient;

    public ApiClient(IOptions<AppSettings> options)
    {
        _settings = options.Value;
        _httpClient = new HttpClient
        {
            BaseAddress = new Uri(_settings.ApiBaseUrl)
        };
    }
}

Persistentie van gebruikersinstellingen

Gebruik voor gebruikersspecifieke instellingen die behouden blijven na app-updates Windows.Storage.ApplicationData (ingepakte apps) of een lokaal JSON-bestand (niet-ingepakte apps):

public class UserSettingsService : IUserSettingsService
{
    private readonly string _settingsPath;

    public UserSettingsService()
    {
        var localAppData = Environment.GetFolderPath(
            Environment.SpecialFolder.LocalApplicationData);
        _settingsPath = Path.Combine(localAppData, "Contoso", "MyApp", "settings.json");
    }

    public async Task SaveAsync<T>(string key, T value)
    {
        var settings = await LoadAllAsync();
        settings[key] = JsonSerializer.Serialize(value);
        Directory.CreateDirectory(Path.GetDirectoryName(_settingsPath)!);
        await File.WriteAllTextAsync(
            _settingsPath, JsonSerializer.Serialize(settings));
    }
}

Opmerking

Verpakte apps (MSIX) kunnen ApplicationData.Current.LocalSettings gebruiken voor eenvoudige sleutel-waardeparen. Uitgepakte apps moeten hun eigen opslaglocatie beheren.

Functievlaggen

Implementeer functievlagmen om geleidelijke implementatie en A/B-tests mogelijk te maken zonder opnieuw te implementeren.

Lokale functievlagmen met configuratie

public interface IFeatureFlagService
{
    bool IsEnabled(string featureName);
}

public class FeatureFlagService : IFeatureFlagService
{
    private readonly Dictionary<string, bool> _flags;

    public FeatureFlagService(IConfiguration configuration)
    {
        _flags = configuration.GetSection("FeatureFlags")
            .Get<Dictionary<string, bool>>() ?? new();
    }

    public bool IsEnabled(string featureName) =>
        _flags.TryGetValue(featureName, out var enabled) && enabled;
}

Integratie van Azure App Configuration

Gebruik Azure App Configuration voor door de cloud beheerde functievlagmen:

dotnet add package Microsoft.Extensions.Configuration.AzureAppConfiguration
dotnet add package Microsoft.FeatureManagement
using Azure.Identity;

Host = Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder()
    .ConfigureAppConfiguration((context, config) =>
    {
        config.AddAzureAppConfiguration(options =>
        {
            options.Connect(
                    new Uri("https://<your-store>.azconfig.io"),
                    new DefaultAzureCredential())
                .UseFeatureFlags(flagOptions =>
                {
                    flagOptions.CacheExpirationInterval = TimeSpan.FromMinutes(5);
                });
        });
    })
    .ConfigureServices((context, services) =>
    {
        services.AddFeatureManagement(context.Configuration);
    })
    .Build();

Opmerking

Voor lokale ontwikkeling kunt u een verbindingsreeks gebruiken in plaats van DefaultAzureCredential. Sla de verbindingsreeks op in een omgevingsvariabele of Windows Credential Manager, nooit in broncodebeheer:

options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))

Tip

Zie Gefaseerde pakketuitrol voor geleidelijke uitrol via de Store op pakketniveau.

Enterprise- en LOB-patronen

Line-Of-Business-apps hebben aanvullende vereisten voor identiteits-, gegevensbeveiliging en apparaatbeheer.

Identiteit en voorwaardelijke toegang

MSAL (Microsoft Authentication Library) gebruiken voor bedrijfsverificatie:

services.AddSingleton<IAuthService>(sp =>
{
    var app = PublicClientApplicationBuilder
        .Create("your-client-id")
        .WithAuthority(AzureCloudInstance.AzurePublic, "your-tenant-id")
        .WithRedirectUri("http://localhost")
        .Build();
    return new AuthService(app);
});

Important

De http://localhost omleidings-URI is geschikt voor ontwikkeling. Gebruik in plaats daarvan de Windows broker (WAM) voor productie-desktop-apps, die eenmalige aanmelding biedt met het Windows-account van de gebruiker en sterkere tokenbeveiliging.

Bedrijfs-apps die zijn geïmplementeerd via Intune, kunnen beleidsregels voor voorwaardelijke toegang afdwingen waarvoor het volgende is vereist:

  • Apparaatcompatibiliteit (versleuteling, pincode, versie van het besturingssysteem)
  • Meervoudige verificatie
  • Netwerklocatiebeperkingen

Offlinegegevens en cacheopslag

Desktop LOB-apps moeten vaak offline werken. Implementeer een opslagplaatspatroon met lokale cache:

public class CachedRepository<T> : IRepository<T> where T : class, IEntity
{
    private readonly IApiClient _apiClient;
    private readonly ILocalDatabase _localDb;

    public async Task<IReadOnlyList<T>> GetAllAsync(bool forceRefresh = false)
    {
        if (!forceRefresh)
        {
            var cached = await _localDb.GetAllAsync<T>();
            if (cached.Any())
                return cached;
        }

        try
        {
            var items = await _apiClient.GetAsync<List<T>>();
            await _localDb.UpsertAllAsync(items);
            return items;
        }
        catch (HttpRequestException)
        {
            // Offline fallback
            return await _localDb.GetAllAsync<T>();
        }
    }
}

Gegevensbeveiliging

Gebruik Windows.Security.Cryptography.DataProtection (verpakte apps) of de .NET DataProtectionProvider voor het versleutelen van gevoelige lokale gegevens.

Installeer het vereiste pakket:

dotnet add package Microsoft.AspNetCore.DataProtection.Extensions

Registreer vervolgens gegevensbeveiliging in uw DI-container:

using Microsoft.AspNetCore.DataProtection;

services.AddDataProtection()
    .SetApplicationName("Contoso.LOBApp")
    .ProtectKeysWithDpapi();

Gelaagde architectuur

Structureer uw WinUI 3-app in lagen om afhankelijkheden in één richting te laten stromen:

┌─────────────────────────────┐
│   Views (XAML + code-behind)│  ← UI layer, no business logic
├─────────────────────────────┤
│   ViewModels (MVVM Toolkit) │  ← Presentation logic, commands
├─────────────────────────────┤
│   Services / Use Cases      │  ← Business rules, orchestration
├─────────────────────────────┤
│   Repositories / Data       │  ← Data access, API clients, caching
└─────────────────────────────┘

Reglement:

  • Elke laag is alleen afhankelijk van de laag direct eronder.
  • ViewModels verwijzen nooit naar UI-typen (Page, Window, ContentDialog).
  • Services definiëren interfaces; implementaties leven in de gegevenslaag.
  • Registreer alle afhankelijkheden tussen lagen in de DI-container.

Compatibiliteit met eerdere versies en versiebeheer

Wanneer u nieuwe versies van uw app vrijgeeft, kunt u het volgende overwegen:

  • Gegevensmigratie: versie van uw lokale databaseschema. Gebruik een migratierunner bij het opstarten om een upgrade uit te voeren van een eerder schema naar het huidige schema.
  • Migratie van instellingen: Sla een schemaversie op in uw instellingenbestand. Bij het laden transformaties van oude formaten naar nieuwe toepassen.
  • Installaties naast elkaar: MSIX werkt standaard een pakket bij. Als u meerdere primaire versies naast elkaar wilt uitvoeren, wijst u elke versie een afzonderlijke pakketfamilienaam toe tijdens het ontwerp.
public class DatabaseMigrator
{
    public async Task MigrateAsync(SqliteConnection db)
    {
        var currentVersion = await GetSchemaVersionAsync(db);

        if (currentVersion < 2)
            await ApplyMigration_v2(db);
        if (currentVersion < 3)
            await ApplyMigration_v3(db);

        await SetSchemaVersionAsync(db, LatestVersion);
    }
}

Uw installatie controleren

Start de app en navigeer naar elke pagina om te controleren of services correct worden opgelost. Als een service niet is geregistreerd, ziet u tijdens de uitvoering een InvalidOperationException waarin de naam van het ontbrekende type wordt vermeld. Controleer ook of:

  • Configuratiewaarden worden geladen vanuit appsettings.json (controleer een gebonden eigenschap in de debugger).
  • Functievlagmen evalueren zoals verwacht (een vlag in- en uitschakelen en opnieuw opstarten).
  • Offlinecaching retourneert gegevens wanneer het netwerk niet beschikbaar is.