本文介绍如何将经过验证的体系结构模式应用于使用 Windows 应用 SDK生成的 WinUI 3 桌面应用。 你将了解如何为企业业务线(LOB)方案设置依赖项注入、管理配置和结构代码。
先决条件
- Windows 应用 SDK 1.5 或更高版本
- .NET 8 或更高版本
- 安装了 .NET 桌面开发 和 Windows 应用程序开发 工作负载的 Visual Studio 2022 版本 17.10 或更高版本
依赖注入
WinUI 3 桌面应用不包含内置的依赖项注入(DI)容器(如 ASP.NET Core),但可以使用同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();
}
}
注释
如果你忘记注册某个服务,上述 GetService<T>() 辅助程序会在运行时引发一个 ArgumentException,其中包含缺失的类型名称。 运行应用并在开发过程中导航到每个页面,以验证所有注册是否正确。
将依赖项注入到 ViewModel 中
配置容器后,ViewModels 通过构造函数注入接收依赖项:
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";
}
}
服务生存期
注册服务时选择适当的生存期:
| 使用寿命 | 方法 | 用于 |
|---|---|---|
| 单例 | AddSingleton<T>() |
导航,应用范围状态,缓存 |
| 作用域 | AddScoped<T>() |
按窗口或按对话框划分的上下文 |
| 瞬态的 | AddTransient<T>() |
ViewModels、无状态服务 |
Tip
将 ViewModel 注册为 暂时性 模型,以便每个导航创建一个新的实例。 将持有应用范围内状态的服务注册为 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));
}
}
注释
打包应用(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();
注释
对于本地开发,可以使用连接字符串而不是 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、OS 版本)
- 多重身份验证
- 网络位置限制
脱机数据和缓存
桌面 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(打包的应用)或.NETDataProtectionProvider来加密敏感的本地数据。
安装所需的包:
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, ,WindowContentDialog)。 - 服务定义接口;实现位于数据层中。
- 在 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加载(在调试器中检查某个已绑定的属性)。 - 功能标志的求值结果符合预期(切换标志并重启)。
- 当网络不可用时,脱机缓存将返回数据。