Pola arsitektur untuk aplikasi desktop WinUI 3

Artikel ini menunjukkan cara menerapkan pola arsitektur yang terbukti ke aplikasi desktop WinUI 3 yang dibangun dengan SDK Aplikasi Windows. Anda akan mempelajari cara menyiapkan injeksi dependensi, mengelola konfigurasi, dan menyusun kode untuk skenario line-of-business (LOB) perusahaan.

Prasyarat

  • SDK Aplikasi Windows 1.5 atau yang lebih baru
  • .NET 8 atau yang lebih baru
  • Visual Studio 2022 versi 17.10 atau yang lebih baru dengan beban kerja pengembangan desktop .NET dan pengembangan aplikasi Windows

Injeksi Ketergantungan

Aplikasi desktop WinUI 3 tidak menyertakan kontainer injeksi dependensi bawaan (DI) seperti yang ASP.NET Core lakukan, tetapi Anda dapat menambahkannya menggunakan paket NuGet yang samaMicrosoft.Extensions.DependencyInjection. DI membuat kode Anda dapat diuji, digabungkan secara longgar, dan lebih mudah dipertahankan.

Siapkan kontainer DI

Instal paket NuGet:

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

Konfigurasikan host dan layanan di :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

Jika Anda lupa mendaftarkan layanan, pembantu GetService<T>() di atas melemparkan ArgumentException pada runtime dengan nama jenis yang hilang. Jalankan aplikasi dan navigasikan ke setiap halaman selama pengembangan untuk memverifikasi bahwa semua pendaftaran sudah benar.

Menyuntikkan dependensi ke dalam ViewModels

Setelah kontainer dikonfigurasi, ViewModels Anda menerima dependensi melalui injeksi konstruktor:

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

Masa pakai layanan

Pilih masa pakai yang sesuai saat mendaftarkan layanan:

Seumur hidup Metode Gunakan untuk
Singleton AddSingleton<T>() Navigasi, status seluruh aplikasi, cache
Dibatasi AddScoped<T>() Konteks per jendela atau per dialog
Sementara AddTransient<T>() ViewModels, layanan tanpa status

Tip

Daftarkan ViewModels sebagai Sementara sehingga setiap navigasi membuat instans baru. Daftarkan layanan yang memiliki status seluruh aplikasi sebagai Singleton.

Manajemen konfigurasi

Gunakan Microsoft.Extensions.Configuration untuk mengelola pengaturan aplikasi di aplikasi desktop, pola yang sama yang digunakan dalam ASP.NET Core.

Menambahkan dukungan konfigurasi

Instal paket yang diperlukan:

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

Buat appsettings.json file di akar proyek Anda. Di Penjelajah Solusi, klik kanan pada file, pilih Properti, lalu setel Salin ke Direktori Output menjadi Salin jika lebih baru. Atau, tambahkan <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> ke entri file di .csproj Anda.

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

Tautkan konfigurasi dalam konfigurasi DI Anda:

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

Menggunakan konfigurasi dalam layanan

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

Persistensi pengaturan pengguna

Untuk pengaturan per pengguna yang bertahan dari pembaruan aplikasi, gunakan Windows.Storage.ApplicationData (aplikasi paket) atau file JSON lokal (aplikasi yang tidak dikemas):

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

Aplikasi paket (MSIX) dapat digunakan ApplicationData.Current.LocalSettings untuk pasangan kunci-nilai sederhana. Aplikasi yang tidak dipaketkan harus mengelola lokasi penyimpanan mereka sendiri.

Bendera fitur

Terapkan bendera fitur untuk mengaktifkan peluncuran bertahap dan pengujian A/B tanpa penyebaran ulang.

Bendera fitur lokal dengan konfigurasi

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

integrasi Azure App Configuration

Untuk bendera fitur yang dikelola cloud, gunakan 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

Untuk pengembangan lokal, Anda dapat menggunakan string koneksi alih-alih DefaultAzureCredential. Simpan string koneksi dalam variabel lingkungan atau Windows Credential Manager — tidak pernah dalam kontrol sumber:

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

Tip

Untuk peluncuran bertahap berbasis Store di tingkat paket, lihat Peluncuran paket bertahap.

Pola perusahaan dan LOB

Aplikasi lini bisnis memiliki persyaratan tambahan sekeliling identitas, perlindungan data, dan manajemen perangkat.

Identitas dan akses bersyarat

Gunakan MSAL (Pustaka Autentikasi Microsoft) untuk autentikasi perusahaan:

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

Penting

URI pengalihan http://localhost sesuai untuk pengembangan. Untuk aplikasi desktop produksi, gunakan broker Windows (WAM) sebagai gantinya, yang menyediakan SSO dengan akun Windows pengguna dan perlindungan token yang lebih kuat.

Aplikasi perusahaan yang disebarkan melalui Intune dapat memberlakukan kebijakan akses bersyarat yang memerlukan:

  • Kepatuhan perangkat (enkripsi, PIN, versi OS)
  • Autentikasi multifaktor
  • Pembatasan lokasi jaringan

Data offline dan cache

Aplikasi LOB desktop sering kali perlu bekerja offline. Terapkan pola repositori dengan cache lokal:

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

Perlindungan data

Gunakan Windows.Security.Cryptography.DataProtection (aplikasi paket) atau .NET DataProtectionProvider untuk mengenkripsi data lokal sensitif.

Instal paket yang diperlukan:

dotnet add package Microsoft.AspNetCore.DataProtection.Extensions

Kemudian daftarkan perlindungan data dalam kontainer DI Anda:

using Microsoft.AspNetCore.DataProtection;

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

Arsitektur berlapis

Susun aplikasi WinUI 3 Anda secara berlapis untuk menjaga dependensi tetap mengalir dalam satu arah:

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

Aturan:

  • Setiap lapisan hanya bergantung pada lapisan tepat di bawahnya.
  • ViewModels tidak pernah mereferensikan jenis UI (Page, Window, ContentDialog).
  • Layanan mendefinisikan antarmuka; implementasi berada di lapisan data.
  • Daftarkan semua dependensi antar-lapisan dalam kontainer DI.

Kompatibilitas mundur dan pembuatan versi

Saat Anda merilis versi baru aplikasi, pertimbangkan:

  • Migrasi data: Versi skema database lokal Anda. Gunakan alat migrasi saat aplikasi dimulai untuk memutakhirkan skema versi sebelumnya apa pun ke versi saat ini.
  • Migrasi pengaturan: Simpan versi skema di file pengaturan Anda. Saat dimuat, terapkan transformasi dari format lama ke yang baru.
  • Penginstalan berdampingan: Secara default, MSIX memutakhirkan paket di tempat. Untuk menjalankan beberapa versi utama secara berdampingan, tetapkan setiap versi nama keluarga paket yang berbeda pada waktu desain.
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);
    }
}

Verifikasi pengaturan Anda

Jalankan aplikasi dan navigasi ke setiap halaman untuk memverifikasi bahwa layanan diselesaikan dengan benar. Jika layanan tidak terdaftar, Anda akan melihat InvalidOperationException pada runtime dengan nama jenis yang hilang. Konfirmasikan juga bahwa:

  • Nilai konfigurasi dimuat dari appsettings.json (periksa properti terikat di debugger).
  • Flag fitur berfungsi sebagaimana mestinya (ubah status flag dan mulai ulang).
  • Cache offline mengembalikan data saat jaringan tidak tersedia.