本文說明如何將經過驗證的架構模式應用於使用 Windows 應用程式 SDK 建置的 WinUI 3 桌面應用程式。 你將學習如何設定相依性注入、管理組態,以及為企業業務線(LOB)情境組織程式碼結構。
先決條件
- Windows 應用程式 SDK 1.5 或更新版本
- .NET 8 或更新版本
- Visual Studio 2022 版本 17.10 或更新版本,支援 .NET 桌面開發及 Windows 應用程式開發工作負載
依賴注入
WinUI 3 桌面應用程式不像 ASP.NET Core 那樣內建相依注入(DI)容器,但你可以用同Microsoft.Extensions.DependencyInjection一個 NuGet 套件新增一個容器。 DI 讓你的程式碼變得可測試、鬆耦合且更易維護。
設定 DI 容器
安裝 NuGet 套件:
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Hosting
在你的 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
如果你忘記註冊服務,上方的 GetService<T>() 輔助程式會在執行階段擲回 ArgumentException,並附上遺漏的型別名稱。 在開發過程中執行應用程式並瀏覽每個頁面,確認所有註冊是否正確。
將依賴項注入至 ViewModels
容器設定完成後,你的 ViewModel 會透過建構器注入接收相依性:
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";
}
}
服務生命週期
註冊服務時請選擇合適的終身期限:
| 有效期間 | 方法 | 用途 |
|---|---|---|
| Singleton | AddSingleton<T>() |
導覽、應用程式整體狀態、快取 |
| 限定範圍 | AddScoped<T>() |
每個視窗或每個對話的上下文 |
| Transient | AddTransient<T>() |
ViewModels,無狀態服務 |
Tip
將 ViewModels 註冊為 暫態 ,讓每次導航都能建立一個全新的實例。 將保存整個應用程式狀態的服務註冊為 Singleton。
組態管理
用Microsoft.Extensions.Configuration來管理桌面應用程式的設定,這和 ASP.NET Core 用的模式一樣。
新增組態支援
安裝所需的套件:
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options
在你的專案根目錄建立 appsettings.json 一個檔案。 在 方案總管 中,以滑鼠右鍵按一下該檔案,選取 屬性,並將 複製到輸出目錄 設定為 如果較新則複製。 或者,將 <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> 新增至 .csproj 中該檔案的項目。
{
"AppSettings": {
"ApiBaseUrl": "https://api.contoso.com/v2",
"MaxRetryCount": 3,
"EnableTelemetry": true
},
"Logging": {
"LogLevel": "Information"
}
}
在你的 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>();
})
在服務中使用設定
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)
};
}
}
使用者設定的持久性
對於在應用程式更新後仍可保留的每位使用者設定,請使用 Windows.Storage.ApplicationData(已封裝應用程式)或本機 JSON 檔案(未封裝應用程式):
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
打包應用程式(MSIX)可以使用 ApplicationData.Current.LocalSettings 來處理簡單的鍵值對。 未封裝的應用程式必須自行管理儲存位置。
功能標幟
實作功能標誌,以促進逐步部署與 A/B 測試,無需重新部署。
本地特徵旗標與配置
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;
}
Azure 應用程式組態整合
對於雲端管理的功能旗標,請使用 Azure 應用程式組態:
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
對於局部開發,你可以使用 連接字串 代替 DefaultAzureCredential。 將 連接字串 儲存在環境變數或 Windows 憑證管理器中——絕不在原始碼控制中:
options.Connect(Environment.GetEnvironmentVariable("APP_CONFIG_CONNECTION_STRING"))
Tip
關於基於商店的套件層級漸進式部署,請參見 漸進式套件部署。
企業與 LOB 模式
業務線上應用程式在身份、資料保護及裝置管理方面有額外需求。
身份與條件存取
使用 MSAL(Microsoft 驗證資源庫)進行企業認證:
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重定向 URI 適合開發。 對於生產型桌面應用程式,請改用 Windows 代理(WAM),它能與使用者的 Windows 帳號進行單點單登入(SSO)並加強令牌保護。
透過 Intune 部署的企業應用程式可強制執行條件存取政策,要求:
- 裝置相容性(加密、PIN、作業系統版本)
- 多因素驗證
- 網路位置限制
離線資料與快取
桌面 LOB 應用程式經常需要離線運作。 實作帶有本地快取的儲存庫模式:
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>();
}
}
}
資料保護
使用Windows.Security.Cryptography.DataProtection(套裝應用程式)或 .NET DataProtectionProvider 來加密敏感的本地資料。
安裝所需的套件:
dotnet add package Microsoft.AspNetCore.DataProtection.Extensions
接著在你的 DI 容器中註冊資料保護:
using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.SetApplicationName("Contoso.LOBApp")
.ProtectKeysWithDpapi();
分層架構
將你的 WinUI 3 應用程式分層結構,讓相依關係保持單向流動:
┌─────────────────────────────┐
│ 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
└─────────────────────────────┘
規則:
- 每一層只依賴於其正下方的那一層。
- ViewModels 從不參考 UI 類型(
Page,Window,ContentDialog)。 - 服務定義介面;實作存在於資料層。
- 在 DI 容器中登錄所有跨層相依關係。
向下相容性與版本控制
當您發布應用程式的新版本時,請考慮:
- 資料遷移:為你本地資料庫架構設定版本。 啟動時使用遷移執行器,從任何先前的架構升級到目前的架構。
- 設定遷移:將結構版本存到你的設定檔案中。 載入時,將舊格式轉換為新格式。
- 並排安裝:MSIX 預設會先升級一個套件。 要並排執行多個主要版本,設計時為每個版本分配一個獨立的套件族名稱。
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);
}
}
驗證您的設定
執行應用程式並瀏覽至各個頁面,確認服務是否都能正確解析。 如果服務未註冊,您會在執行階段看到一個 InvalidOperationException,其中會包含缺少的型別名稱。 同時確認:
- 設定值會從
appsettings.json載入(請在除錯器中檢查已繫結的屬性)。 - 功能旗標會如預期般生效(切換旗標後重新啟動)。
- 當網路無法使用時,離線快取會回傳資料。