Arkitekturmönster för WinUI 3-skrivbordsappar

Den här artikeln visar hur du tillämpar beprövade arkitekturmönster på WinUI 3-skrivbordsappar som skapats med Windows App SDK. Du får lära dig hur du konfigurerar beroendeinmatning, hanterar konfiguration och strukturkod för verksamhetsspecifika scenarier (LOB).

Förutsättningar

  • Windows App SDK 1,5 eller senare
  • .NET 8 eller senare
  • Visual Studio 2022 version 17.10 eller senare med arbetsbelastningar för .NET skrivbordsutveckling och Windows programutveckling

Beroendeinsprutning

WinUI 3-skrivbordsappar innehåller inte en inbyggd di-container (beroendeinmatning) som ASP.NET Core gör, men du kan lägga till en med samma Microsoft.Extensions.DependencyInjection NuGet-paket. DI gör koden testbar, löst kopplad och enklare att underhålla.

Konfigurera en DI-container

Installera NuGet-paketet:

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

Konfigurera värden och tjänster i din 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();
    }
}

Note

Om du glömmer att registrera en tjänst utlöser hjälpmetoden GetService<T>() ovan ett ArgumentException under körning med namnet på den saknade typen. Kör appen och gå till varje sida under utvecklingen för att kontrollera att alla registreringar är korrekta.

Injicera beroenden i ViewModels

När containern har konfigurerats tar ViewModels emot beroenden via konstruktorinmatning:

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";
    }
}

Tjänstlivslängd

Välj lämplig livslängd när du registrerar tjänster:

Livstid Metod Använd för
Singleton AddSingleton<T>() Navigering, applikationsövergripande tillstånd, cacheminnen
Omfattad AddScoped<T>() Kontexter per fönster eller per dialogruta
Transient AddTransient<T>() ViewModels, tillståndslösa tjänster

Tip

Registrera ViewModels som Tillfälligt så att varje navigering skapar en ny instans. Registrera tjänster som har appomfattande tillstånd som Singleton.

Konfigurationshantering

Använd Microsoft.Extensions.Configuration för att hantera appinställningar i skrivbordsappar, samma mönster som används i ASP.NET Core.

Lägga till konfigurationsstöd

Installera de paket som krävs:

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

Skapa en appsettings.json fil i projektroten. I Prieskumník riešení högerklickar du på filen, väljer Egenskaper och anger Kopiera till Utdatakatalog till Kopiera om det är nyare. Alternativt kan du lägga till <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> i filens post i din .csproj.

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

Bind konfigurationen i din DI-konfiguration:

.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>();
})

Använda konfiguration i tjänster

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)
        };
    }
}

Beständighet för användarinställningar

För inställningar per användare som överlever appuppdateringar använder du Windows.Storage.ApplicationData (paketerade appar) eller en lokal JSON-fil (uppackade appar):

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));
    }
}

Note

Paketerade appar (MSIX) kan användas ApplicationData.Current.LocalSettings för enkla nyckel/värde-par. Appar utan paketering måste hantera sin egen lagringsplats.

Funktionsflaggor (Feature flags)

Implementera funktionsflaggor för att aktivera gradvis distribution och A/B-testning utan omdistribution.

Lokala funktionsflaggor med konfiguration

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;
}

Integrering med Azure App Configuration

För molnhanterade funktionsflaggor använder du Azure App Configuration:

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();

Note

För lokal utveckling kan du använda en anslutningssträng i stället för DefaultAzureCredential. Lagra reťazec pripojenia i en miljövariabel eller Windows Credential Manager – aldrig i källkontrollen:

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

Tip

För butiksbaserad gradvis utrullning på paketnivå, se Gradvis paketutrullning.

Företags- och LOB-mönster

Verksamhetsspecifika appar har ytterligare krav på identitets-, dataskydds- och enhetshantering.

Identitet och villkorlig åtkomst

Använd MSAL (Microsofts autentiseringsbibliotek) för företagsautentisering:

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

Omdirigerings-URI http://localhost :n är lämplig för utveckling. För skrivbordsappar för produktion använder du i stället Windows broker (WAM), som tillhandahåller enkel inloggning med användarens Windows-konto och starkare tokenskydd.

Företagsappar som distribueras via Intune kan tillämpa principer för villkorlig åtkomst som kräver:

  • Enhetsefterlevnad (kryptering, PIN-kod, OS-version)
  • Multifaktorautentisering
  • Begränsningar för nätverksplats

Offlinedata och cachelagring

Skrivbordsspecifika appar måste ofta arbeta offline. Implementera ett databasmönster med lokal cachelagring:

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>();
        }
    }
}

Dataskydd

Använd Windows.Security.Cryptography.DataProtection (paketerade appar) eller .NET DataProtectionProvider för kryptering av känsliga lokala data.

Installera det nödvändiga paketet:

dotnet add package Microsoft.AspNetCore.DataProtection.Extensions

Registrera sedan dataskydd i din DI-container:

using Microsoft.AspNetCore.DataProtection;

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

Skiktad arkitektur

Strukturera din WinUI 3-app i lager så att beroenden flödar i en riktning:

┌─────────────────────────────┐
│   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
└─────────────────────────────┘

Reglemente:

  • Varje lager beror bara på lagret direkt under det.
  • ViewModels refererar aldrig till användargränssnittstyper (Page, Window, ContentDialog).
  • Tjänster definierar gränssnitt. implementeringar finns i datalagret.
  • Registrera alla beroenden mellan lager i DI-containern.

Bakåtkompatibilitet och versionshantering

När du släpper nya versioner av din app bör du tänka på:

  • Datamigrering: Version av ditt lokala databasschema. Använd en migreringslöpare vid start för att uppgradera från ett tidigare schema till det aktuella.
  • Inställningsmigrering: Lagra en schemaversion i inställningsfilen. Vid inläsning tillämpar du transformeringar från gamla format till nya.
  • Sida vid sida-installationer: MSIX uppgraderar ett paket på plats som standard. Om du vill köra flera större versioner sida vid sida tilldelar du varje version ett distinkt paketfamiljenamn vid designtillfället.
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);
    }
}

Kontrollera konfigurationen

Kör appen och navigera till varje sida för att verifiera att tjänsterna löses korrekt. Om en tjänst inte är registrerad visas ett InvalidOperationException under körning med namnet på den saknade typen. Bekräfta också att:

  • Konfigurationsvärden läses in från appsettings.json (kontrollera en bunden egenskap i felsökningsprogrammet).
  • Funktionsflaggor fungerar som förväntat (ändra en flagga och starta om).
  • Cachelagring offline levererar data när nätverket inte är tillgängligt.