Vzory architektury pro desktopové aplikace WinUI 3

Tento článek ukazuje, jak použít osvědčené vzory architektury pro desktopové aplikace WinUI 3 vytvořené pomocí Windows App SDK. Dozvíte se, jak nastavit vkládání závislostí, spravovat konfiguraci a strukturovat kód pro podnikové provozní scénáře (LOB).

Předpoklady

  • Windows App SDK 1.5 nebo novější
  • .NET 8 nebo novější
  • Visual Studio 2022 verze 17.10 nebo novější s úlohami vývoje .NET desktopů a Windows aplikací

Injektáž závislostí

Desktopové aplikace WinUI 3 neobsahují integrovaný kontejner injektáže závislostí (DI), jako je ASP.NET Core, ale můžete ho přidat pomocí stejného Microsoft.Extensions.DependencyInjection balíčku NuGet. Di umožňuje testovatelný, volně propojený a snadněji udržovatelný kód.

Nastavení kontejneru DI

Nainstalujte balíček NuGet:

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

Nakonfigurujte hostitele a služby ve vašem 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

Pokud zapomenete zaregistrovat službu, výše uvedený pomocník GetService<T>() za běhu vyvolá výjimku ArgumentException s názvem chybějícího typu. Spusťte aplikaci a během vývoje přejděte na každou stránku, abyste ověřili správnost všech registrací.

Vložení závislostí do modelů ViewModels

Když je kontejner nakonfigurovaný, objekty ViewModels přijímají závislosti prostřednictvím injektáže konstruktoru:

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

Životnost služeb

Při registraci služeb zvolte odpovídající dobu životnosti:

Životnost Metoda Použít pro
Singleton AddSingleton<T>() Navigace, stav celé aplikace, mezipaměti
Rozsahem AddScoped<T>() Kontexty pro jednotlivá okna nebo dialogy
Přechodný AddTransient<T>() ViewModels, bezstavové služby

Tip

Zaregistrujte modely ViewModel jako přechodné , aby každá navigace vytvořila novou instanci. Zaregistrujte služby, které uchovávají stav celé aplikace jako Singleton.

Správa konfigurace

Slouží Microsoft.Extensions.Configuration ke správě nastavení aplikací v desktopových aplikacích, stejně jako v ASP.NET Core.

Přidání podpory konfigurace

Nainstalujte požadované balíčky:

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

Vytvořte appsettings.json soubor v kořenovém adresáři projektu. V Průzkumník řešení klikněte na soubor pravým tlačítkem myši, vyberte Vlastnosti a nastavte Kopírovat do výstupního adresáře na Kopírovat, pokud je novější. Případně přidejte <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> k záznamu souboru v .csproj.

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

Vytvořte vazbu konfigurace v nastavení DI:

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

Použití konfigurace ve službách

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

Trvalost uživatelských nastavení

Pro uživatelská nastavení, která přežijí aktualizace aplikací, použijte Windows.Storage.ApplicationData (zabalené aplikace) nebo místní soubor JSON (rozbalené aplikace):

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

Zabalené aplikace (MSIX) mohou používat ApplicationData.Current.LocalSettings pro jednoduché dvojice klíč–hodnota. Rozbalené aplikace musí spravovat vlastní umístění úložiště.

Hlavní příznaky

Implementujte příznaky funkcí, které umožňují postupné zavádění a testování A/B bez opětovného nasazení.

Lokální přepínače funkcí s konfigurací

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

Integrace služby Azure App Configuration

U příznaků funkcí spravovaných v cloudu použijte 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

Pro místní vývoj můžete místo toho použít připojovací řetězec DefaultAzureCredential. Uložte připojovací řetězec do proměnné prostředí nebo do Správce přihlašovacích údajů systému Windows — nikdy ne do systému správy verzí:

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

Tip

Informace o postupném zavádění založeném na obchodě na úrovni balíčků najdete v části Postupné zavádění balíčků.

Podnikové a LOB vzory

Obchodní aplikace mají další požadavky na identitu, ochranu dat a správu zařízení.

Identita a podmíněný přístup

Pro podnikové ověřování použijte knihovnu MSAL (Identity a ověřování Microsoftu):

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

http://localhost identifikátor URI pro přesměrování je vhodný pro vývoj. V případě produkčních desktopových aplikací použijte místo toho zprostředkovatele Windows (WAM), který poskytuje jednotné přihlašování s účtem Windows uživatele a silnější ochranou tokenů.

Podnikové aplikace nasazené prostřednictvím Intune můžou vynutit zásady podmíněného přístupu, které vyžadují:

  • Dodržování předpisů zařízením (šifrování, PIN kód, verze operačního systému)
  • Vícefaktorové ověřování
  • Omezení síťového umístění

Offline data a ukládání do cache

Desktopové aplikace LOB často potřebují fungovat offline. Implementujte vzor repozitáře s lokální mezipamětí:

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

Ochrana dat

Používejte Windows.Security.Cryptography.DataProtection (zabalené aplikace) nebo .NET DataProtectionProvider pro šifrování citlivých místních dat.

Nainstalujte požadovaný balíček:

dotnet add package Microsoft.AspNetCore.DataProtection.Extensions

Pak zaregistrujte ochranu dat v kontejneru DI:

using Microsoft.AspNetCore.DataProtection;

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

Vícevrstvé architektury

Strukturujte aplikaci WinUI 3 ve vrstvách a udržujte tak závislosti v jednom směru:

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

Pravidla:

  • Každá vrstva závisí pouze na vrstvě přímo pod ní.
  • Modely ViewModel nikdy neodkazují na typy uživatelského rozhraní (Page, Window, ContentDialog).
  • Služby definují rozhraní; implementace žijí v datové vrstvě.
  • Zaregistrujte všechny závislosti mezi vrstvami v kontejneru DI.

Zpětná kompatibilita a správa verzí

Při vydání nových verzí aplikace zvažte:

  • Migrace dat: Verzujte schéma místní databáze. Pomocí spouštěče migrace při spuštění proveďte upgrade z předchozího schématu na aktuální schéma.
  • Migrace nastavení: Uložte verzi schématu do souboru nastavení. Při načítání použijte transformace ze starých formátů na nové.
  • Souběžné instalace: MSIX ve výchozím nastavení upgraduje balíček. Pokud chcete spustit více hlavních verzí vedle sebe, přiřaďte každé verzi jedinečný název rodiny balíčků v době návrhu.
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);
    }
}

Ověření nastavení

Spusťte aplikaci a projděte všechny stránky, abyste ověřili, že se služby správně načítají. Pokud služba není zaregistrovaná, za běhu se zobrazí InvalidOperationException s názvem chybějícího typu. Také potvrďte, že:

  • Hodnoty konfigurace se načítají z appsettings.json (zkontrolujte vázanou vlastnost v ladicím programu).
  • Příznaky funkcí se vyhodnocují podle očekávání (přepněte příznak a restartujte aplikaci).
  • Ukládání do mezipaměti v režimu offline poskytuje data, když není k dispozici síť.