WPF 配置与设置管理
大约 12 分钟约 3638 字
WPF 配置与设置管理
简介
WPF 应用的配置管理涵盖应用配置(appsettings.json)、用户设置(Properties.Settings)、环境变量和命令行参数。合理的配置管理有助于区分开发/测试/生产环境,并支持用户个性化设置持久化。在工业上位机场景中,配置管理涉及数据库连接串、设备通信参数、报警阈值、界面布局偏好等多种配置来源,需要统一的管理策略。
配置管理的层次
┌─────────────────────────────────────────┐
│ 命令行参数(最高优先级) │
├─────────────────────────────────────────┤
│ 环境变量 │
├─────────────────────────────────────────┤
│ 环境特定配置文件 │
│ appsettings.Production.json │
├─────────────────────────────────────────┤
│ 默认配置文件 │
│ appsettings.json │
├─────────────────────────────────────────┤
│ 代码默认值(最低优先级) │
└─────────────────────────────────────────┘.NET 配置体系遵循"后添加的覆盖先添加的"原则。后面注册的配置源中的值会覆盖前面注册的配置源中的同名键。
应用配置 vs 用户设置
| 类型 | 存储位置 | 作用域 | 典型内容 |
|---|---|---|---|
| 应用配置 | appsettings.json | 全部用户 | 数据库连接串、设备参数、日志级别 |
| 用户设置 | 用户 AppData | 单个用户 | 窗口位置、主题偏好、最近打开文件 |
| 环境变量 | 操作系统环境 | 运行时 | 环境(Dev/Prod)、密钥、功能开关 |
特点
实现
appsettings.json 配置
多环境配置文件
// ========== appsettings.json(默认配置)==========
{
"App": {
"Title": "设备监控系统",
"Version": "2.1.0",
"Culture": "zh-CN"
},
"ConnectionStrings": {
"Default": "Server=localhost;Database=ScadaDb;Trusted_Connection=True;TrustServerCertificate=True",
"Redis": "localhost:6379"
},
"Device": {
"PollingInterval": 1000,
"RetryCount": 3,
"Timeout": 5000,
"MaxConcurrentConnections": 10
},
"Alarm": {
"Enabled": true,
"CriticalLevelTimeout": 30,
"WarningLevelTimeout": 300,
"SoundEnabled": true,
"EmailNotification": false
},
"Logging": {
"LogLevel": "Information",
"FilePath": "logs/app.log",
"MaxFileSizeMB": 100,
"RetainedDays": 30
},
"UI": {
"DefaultTheme": "Light",
"AutoRefreshInterval": 5000,
"MaxDataPointsPerChart": 1000
}
}// ========== appsettings.Development.json(开发环境)==========
{
"Logging": {
"LogLevel": "Debug"
},
"ConnectionStrings": {
"Default": "Server=dev-sql;Database=ScadaDb_Dev;Trusted_Connection=True"
},
"Device": {
"PollingInterval": 2000,
"Timeout": 10000
}
}// ========== appsettings.Production.json(生产环境)==========
{
"Logging": {
"LogLevel": "Warning"
},
"Device": {
"PollingInterval": 500,
"Timeout": 3000,
"RetryCount": 5
}
}强类型配置模型
// ========== 配置模型类 ==========
/// <summary>
/// 顶层配置模型
/// </summary>
public class AppSettings
{
public const string SectionName = "App";
public AppInfo App { get; set; } = new();
public ConnectionStrings ConnectionStrings { get; set; } = new();
public DeviceSettings Device { get; set; } = new();
public AlarmSettings Alarm { get; set; } = new();
public LogSettings Logging { get; set; } = new();
public UISettings UI { get; set; } = new();
}
public class AppInfo
{
public string Title { get; set; } = "设备监控系统";
public string Version { get; set; } = "1.0.0";
public string Culture { get; set; } = "zh-CN";
}
public class ConnectionStrings
{
public string Default { get; set; } = "";
public string Redis { get; set; } = "";
}
public class DeviceSettings
{
public int PollingInterval { get; set; } = 1000;
public int RetryCount { get; set; } = 3;
public int Timeout { get; set; } = 5000;
public int MaxConcurrentConnections { get; set; } = 10;
/// <summary>
/// 验证配置值是否合法
/// </summary>
public void Validate()
{
if (PollingInterval < 100)
throw new InvalidOperationException("PollingInterval 不能小于 100ms");
if (RetryCount < 0 || RetryCount > 10)
throw new InvalidOperationException("RetryCount 应在 0-10 之间");
if (Timeout < 1000)
throw new InvalidOperationException("Timeout 不能小于 1000ms");
}
}
public class AlarmSettings
{
public bool Enabled { get; set; } = true;
public int CriticalLevelTimeout { get; set; } = 30;
public int WarningLevelTimeout { get; set; } = 300;
public bool SoundEnabled { get; set; } = true;
public bool EmailNotification { get; set; } = false;
}
public class LogSettings
{
public string LogLevel { get; set; } = "Information";
public string FilePath { get; set; } = "logs/app.log";
public int MaxFileSizeMB { get; set; } = 100;
public int RetainedDays { get; set; } = 30;
}
public class UISettings
{
public string DefaultTheme { get; set; } = "Light";
public int AutoRefreshInterval { get; set; } = 5000;
public int MaxDataPointsPerChart { get; set; } = 1000;
}IOptions 模式绑定
// ========== 使用 IOptions<T> 模式绑定配置 ==========
// NuGet: Microsoft.Extensions.Options, Microsoft.Extensions.Options.ConfigurationExtensions
/// <summary>
/// 配置服务注册
/// </summary>
public static class ConfigurationExtensions
{
public static IServiceCollection AddAppConfiguration(
this IServiceCollection services, IConfiguration configuration)
{
// 绑定整个 AppSettings
services.Configure<AppSettings>(configuration);
// 也可以绑定子节点
services.Configure<DeviceSettings>(
configuration.GetSection("Device"));
services.Configure<AlarmSettings>(
configuration.GetSection("Alarm"));
// 注册为单例(直接注入 AppSettings)
services.AddSingleton(sp =>
configuration.Get<AppSettings>() ?? new AppSettings());
return services;
}
}
/// <summary>
/// 在服务中使用 IOptions<T>
/// </summary>
public class DevicePollingService
{
private readonly DeviceSettings _settings;
// 方式 1:直接注入 AppSettings
public DevicePollingService(AppSettings settings)
{
_settings = settings.Device;
}
// 方式 2:注入 IOptions<DeviceSettings>(支持配置热更新时用 IOptionsMonitor)
public DevicePollingService(IOptions<DeviceSettings> options)
{
_settings = options.Value;
}
// 方式 3:注入 IOptionsMonitor<DeviceSettings>(支持热更新)
public DevicePollingService(IOptionsMonitor<DeviceSettings> monitor)
{
_settings = monitor.CurrentValue;
monitor.OnChange(newSettings =>
{
// 配置变更时的回调
_settings = newSettings;
Console.WriteLine($"轮询间隔已更新为: {newSettings.PollingInterval}ms");
});
}
}App.xaml.cs 中加载配置
// ========== App.xaml.cs — 配置加载 ==========
public partial class App : Application
{
public static IConfiguration Configuration { get; private set; } = null!;
public static AppSettings Settings { get; private set; } = new();
private ServiceProvider? _serviceProvider;
protected override void OnStartup(StartupEventArgs e)
{
// 确定当前环境
var env = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT")
?? Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")
?? "Production";
// 构建配置
Configuration = new ConfigurationBuilder()
.SetBasePath(AppDomain.CurrentDomain.BaseDirectory)
// 默认配置文件
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
// 环境特定配置文件(覆盖默认配置)
.AddJsonFile($"appsettings.{env}.json", optional: true, reloadOnChange: true)
// 环境变量(前缀为 App_ 的环境变量)
.AddEnvironmentVariables(prefix: "App_")
// 命令行参数
.AddCommandLine(e.Args)
.Build();
// 绑定为强类型
Settings = Configuration.Get<AppSettings>() ?? new AppSettings();
// 验证配置
Settings.Device.Validate();
// 注册 DI
var services = new ServiceCollection();
services.AddSingleton(Configuration);
services.AddAppConfiguration(Configuration);
_serviceProvider = services.BuildServiceProvider();
base.OnStartup(e);
}
}用户设置持久化
Properties.Settings 使用
// ========== 用户设置服务 ==========
/// <summary>
/// 用户设置服务 — 管理窗口位置、主题偏好等用户级设置
/// Settings.settings 文件中需要定义对应的 User 级设置项
/// </summary>
public class UserSettingsService
{
// ===== 窗口位置 =====
public double WindowWidth
{
get => Properties.Settings.Default.WindowWidth;
set => Properties.Settings.Default.WindowWidth = value;
}
public double WindowHeight
{
get => Properties.Settings.Default.WindowHeight;
set => Properties.Settings.Default.WindowHeight = value;
}
public double WindowX
{
get => Properties.Settings.Default.WindowX;
set => Properties.Settings.Default.WindowX = value;
}
public double WindowY
{
get => Properties.Settings.Default.WindowY;
set => Properties.Settings.Default.WindowY = value;
}
public WindowState WindowState
{
get => (WindowState)Properties.Settings.Default.WindowState;
set => Properties.Settings.Default.WindowState = (int)value;
}
// ===== 主题与外观 =====
public string Theme
{
get => Properties.Settings.Default.Theme;
set => Properties.Settings.Default.Theme = value;
}
public double FontSize
{
get => Properties.Settings.Default.FontSize;
set => Properties.Settings.Default.FontSize = value;
}
// ===== 业务设置 =====
public string LastDeviceId
{
get => Properties.Settings.Default.LastDeviceId;
set => Properties.Settings.Default.LastDeviceId = value;
}
// ===== 最近打开文件 =====
public StringCollection RecentFiles
{
get => Properties.Settings.Default.RecentFiles ?? new StringCollection();
set => Properties.Settings.Default.RecentFiles = value;
}
/// <summary>
/// 保存窗口位置(在 Window Closing 事件中调用)
/// </summary>
public void SaveWindowPosition(Window window)
{
// 只在非最大化/最小化时保存位置
if (window.WindowState == WindowState.Normal)
{
WindowWidth = window.Width;
WindowHeight = window.Height;
WindowX = window.Left;
WindowY = window.Top;
}
WindowState = window.WindowState;
Save();
}
/// <summary>
/// 恢复窗口位置(在 Window Loaded 事件中调用)
/// </summary>
public void RestoreWindowPosition(Window window)
{
window.Width = WindowWidth;
window.Height = WindowHeight;
window.Left = WindowX;
window.Top = WindowY;
window.WindowState = WindowState;
// 确保窗口在屏幕内
EnsureWindowOnScreen(window);
}
/// <summary>
/// 添加最近打开的文件
/// </summary>
public void AddRecentFile(string filePath)
{
var recent = RecentFiles;
if (recent.Contains(filePath))
recent.Remove(filePath);
recent.Insert(0, filePath);
while (recent.Count > 10)
recent.RemoveAt(recent.Count - 1);
RecentFiles = recent;
Save();
}
/// <summary>
/// 获取最近打开的文件列表
/// </summary>
public string[] GetRecentFiles()
{
var recent = RecentFiles;
var result = new string[recent.Count];
for (int i = 0; i < recent.Count; i++)
result[i] = recent[i];
return result;
}
/// <summary>
/// 持久化设置到磁盘
/// </summary>
public void Save()
{
Properties.Settings.Default.Save();
}
/// <summary>
/// 重置为默认值
/// </summary>
public void Reset()
{
Properties.Settings.Default.Reset();
}
/// <summary>
/// 确保窗口在可见屏幕范围内
/// </summary>
private void EnsureWindowOnScreen(Window window)
{
var screen = SystemParameters.WorkArea;
if (window.Left < screen.Left)
window.Left = screen.Left;
if (window.Top < screen.Top)
window.Top = screen.Top;
if (window.Left + window.Width > screen.Right)
window.Left = screen.Right - window.Width;
if (window.Top + window.Height > screen.Bottom)
window.Top = screen.Bottom - window.Height;
}
}自定义 JSON 用户设置
// ========== 自定义 JSON 用户设置(替代 Properties.Settings)==========
/// <summary>
/// 基于 JSON 的用户设置存储
/// 更灵活,支持嵌套结构和自定义序列化
/// </summary>
public class JsonUserSettings<T> where T : class, new()
{
private readonly string _filePath;
private T _settings;
private readonly object _lock = new();
public JsonUserSettings(string fileName = "user-settings.json")
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
var appFolder = Path.Combine(appData, Assembly.GetEntryAssembly()?.GetName().Name ?? "MyApp");
if (!Directory.Exists(appFolder))
Directory.CreateDirectory(appFolder);
_filePath = Path.Combine(appFolder, fileName);
_settings = Load();
}
public T Current
{
get
{
lock (_lock)
{
return _settings;
}
}
}
/// <summary>
/// 更新设置
/// </summary>
public void Update(Action<T> updateAction)
{
lock (_lock)
{
updateAction(_settings);
Save();
}
}
private T Load()
{
if (!File.Exists(_filePath))
return new T();
try
{
var json = File.ReadAllText(_filePath);
return JsonSerializer.Deserialize<T>(json) ?? new T();
}
catch
{
return new T();
}
}
private void Save()
{
var json = JsonSerializer.Serialize(_settings, new JsonSerializerOptions
{
WriteIndented = true,
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
});
File.WriteAllText(_filePath, json);
}
}
// 使用示例
public class MyUserSettings
{
public string Theme { get; set; } = "Light";
public double WindowWidth { get; set; } = 1200;
public double WindowHeight { get; set; } = 800;
public double WindowX { get; set; } = 100;
public double WindowY { get; set; } = 100;
public List<string> RecentFiles { get; set; } = new();
public Dictionary<string, string> ColumnWidths { get; set; } = new();
}
// 在 DI 中注册
services.AddSingleton(new JsonUserSettings<MyUserSettings>());配置变更通知
监控配置文件变更
// ========== 配置热更新 ==========
/// <summary>
/// 配置变更通知服务 — 监控 appsettings.json 变更并通知订阅者
/// </summary>
public class ConfigChangeNotifier : IDisposable
{
private readonly IConfiguration _configuration;
private readonly ILogger<ConfigChangeNotifier> _logger;
private readonly IDisposable _changeTokenRegistration;
private readonly CancellationTokenSource _cts = new();
public event Action<string, object?>? SettingChanged;
public ConfigChangeNotifier(IConfiguration configuration, ILogger<ConfigChangeNotifier> logger)
{
_configuration = configuration;
_logger = logger;
// 注册配置变更回调
RegisterChangeCallback();
}
private void RegisterChangeCallback()
{
var changeToken = _configuration.GetReloadToken();
_changeTokenRegistration = changeToken.RegisterChangeCallback(OnConfigurationChanged, null);
}
private void OnConfigurationChanged(object? state)
{
_logger.LogInformation("检测到配置文件变更");
// 读取变更后的配置
var newSettings = _configuration.Get<AppSettings>();
if (newSettings != null)
{
// 通知订阅者
SettingChanged?.Invoke("AppSettings", newSettings);
}
// 重新注册监听(变更回调是一次性的)
RegisterChangeCallback();
}
public void Dispose()
{
_changeTokenRegistration?.Dispose();
_cts.Dispose();
}
}
// 在 ViewModel 中响应配置变更
public class DashboardViewModel : ObservableObject, IDisposable
{
private readonly ConfigChangeNotifier _notifier;
private int _refreshInterval;
public DashboardViewModel(ConfigChangeNotifier notifier)
{
_notifier = notifier;
_notifier.SettingChanged += OnSettingChanged;
}
private void OnSettingChanged(string key, object? newValue)
{
if (key == "AppSettings" && newValue is AppSettings settings)
{
_refreshInterval = settings.UI.AutoRefreshInterval;
OnPropertyChanged(nameof(RefreshInterval));
}
}
public int RefreshInterval => _refreshInterval;
public void Dispose()
{
_notifier.SettingChanged -= OnSettingChanged;
}
}敏感信息保护
// ========== 敏感配置信息保护 ==========
/// <summary>
/// 配置加密工具 — 使用 DPAPI 保护敏感配置
/// </summary>
public static class ConfigEncryption
{
/// <summary>
/// 加密字符串(使用 Windows DPAPI)
/// </summary>
public static string Encrypt(string plainText)
{
if (string.IsNullOrEmpty(plainText)) return plainText;
var bytes = Encoding.UTF8.GetBytes(plainText);
var encrypted = ProtectedData.Protect(bytes, optionalEntropy: null, scope: DataProtectionScope.CurrentUser);
return Convert.ToBase64String(encrypted);
}
/// <summary>
/// 解密字符串
/// </summary>
public static string Decrypt(string encryptedText)
{
if (string.IsNullOrEmpty(encryptedText)) return encryptedText;
try
{
var bytes = Convert.FromBase64String(encryptedText);
var decrypted = ProtectedData.Unprotect(bytes, optionalEntropy: null, scope: DataProtectionScope.CurrentUser);
return Encoding.UTF8.GetString(decrypted);
}
catch
{
return encryptedText; // 解密失败返回原文
}
}
}
/// <summary>
/// 安全配置提供器 — 自动加密/解密敏感字段
/// </summary>
public class SecureConnectionStrings
{
private readonly Dictionary<string, string> _connections = new();
/// <summary>
/// 设置连接字符串(自动加密保存)
/// </summary>
public void Set(string name, string connectionString)
{
_connections[name] = ConfigEncryption.Encrypt(connectionString);
}
/// <summary>
/// 获取连接字符串(自动解密)
/// </summary>
public string Get(string name)
{
return _connections.TryGetValue(name, out var encrypted)
? ConfigEncryption.Decrypt(encrypted)
: "";
}
}命令行参数处理
// ========== 命令行参数处理 ==========
/// <summary>
/// 启动参数模型
/// </summary>
public class StartupOptions
{
[Option('e', "env", Default = "Production", HelpText = "运行环境")]
public string Environment { get; set; } = "Production";
[Option('p', "port", Default = 0, HelpText = "服务端口")]
public int Port { get; set; }
[Option('d', "debug", Default = false, HelpText = "启用调试模式")]
public bool Debug { get; set; }
[Option('c', "config", Default = "", HelpText = "自定义配置文件路径")]
public string ConfigPath { get; set; } = "";
[Option('v', "verbose", Default = false, HelpText = "详细日志")]
public bool Verbose { get; set; }
[Value(0, MetaName = "files", HelpText = "要打开的文件")]
public IEnumerable<string> Files { get; set; } = Array.Empty<string>();
}
// 解析命令行参数
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
// 使用 CommandLineParser 解析
var parser = new Parser(with => with.HelpWriter = Console.Out);
var parseResult = parser.ParseArguments<StartupOptions>(e.Args);
if (parseResult.Errors.Any())
{
// 参数错误,显示帮助
return;
}
var options = parseResult.Value;
// 根据命令行参数覆盖配置
if (options.Verbose)
{
// 调整日志级别为 Debug
}
if (!string.IsNullOrEmpty(options.ConfigPath))
{
// 使用自定义配置文件
}
// 打开指定文件
foreach (var file in options.Files)
{
OpenFile(file);
}
base.OnStartup(e);
}
}配置验证
// ========== 配置验证 ==========
/// <summary>
/// 配置验证服务 — 启动时验证必要配置项
/// </summary>
public class ConfigurationValidator
{
private readonly IConfiguration _configuration;
private readonly ILogger<ConfigurationValidator> _logger;
private readonly List<string> _errors = new();
public ConfigurationValidator(IConfiguration configuration, ILogger<ConfigurationValidator> logger)
{
_configuration = configuration;
_logger = logger;
}
/// <summary>
/// 验证所有配置项
/// </summary>
public bool Validate()
{
_errors.Clear();
ValidateRequired("ConnectionStrings:Default", "数据库连接字符串");
ValidateRequired("ConnectionStrings:Redis", "Redis 连接字符串");
ValidateRange("Device:PollingInterval", 100, 60000, "设备轮询间隔");
ValidateRange("Device:Timeout", 1000, 60000, "设备超时时间");
ValidateRange("Device:RetryCount", 0, 10, "设备重试次数");
if (_errors.Count > 0)
{
_logger.LogError("配置验证失败: {Errors}", string.Join("; ", _errors));
return false;
}
_logger.LogInformation("配置验证通过");
return true;
}
private void ValidateRequired(string key, string description)
{
var value = _configuration[key];
if (string.IsNullOrEmpty(value))
_errors.Add($"{description}({key})未配置");
}
private void ValidateRange(string key, int min, int max, string description)
{
var value = _configuration.GetValue<int>(key);
if (value < min || value > max)
_errors.Add($"{description}({key})值 {value} 不在范围 [{min}, {max}] 内");
}
}优点
缺点
总结
WPF 配置管理使用 appsettings.json 管理应用级配置,Properties.Settings 或自定义 JSON 管理用户级设置。通过 IConfigurationBuilder 支持多环境、环境变量和命令行参数覆盖。建议将配置绑定为强类型模型(Options 模式),敏感信息使用 DPAPI 加密。启动时进行配置验证,确保必要配置项完整。
关键知识点
- .NET 配置体系(IConfiguration)支持 JSON、环境变量、命令行等多种来源。
- Properties.Settings 的 User 级设置保存在用户 AppData 目录。
- AddJsonFile 的 reloadOnChange 参数支持文件变更时自动重载。
- 配置优先级:命令行 > 环境变量 > 环境特定 JSON > 默认 JSON。
IOptionsMonitor<T>支持配置热更新回调。- DPAPI(ProtectedData)可以在 Windows 上加密敏感配置。
项目落地视角
- 为不同环境(Development/Staging/Production)准备独立配置文件。
- 使用强类型配置模型(
IOptions<T>)而非字符串索引访问。 - 敏感信息(数据库密码、API Key)使用 DPAPI 或 Azure Key Vault。
- 启动时进行配置验证,缺少必要配置时给出明确提示。
- 配置变更日志记录,方便排查配置漂移问题。
常见误区
- 把密码明文写在 appsettings.json 中提交到代码仓库。
- 修改 appsettings.json 后忘记设置"复制到输出目录"。
- 使用 Properties.Settings 存储大量数据(应使用数据库)。
- 忽略配置验证,缺少必要配置时应用行为不可预测。
- 多环境配置文件不一致导致生产环境问题。
进阶路线
- 学习 Azure Key Vault 和 Secret Manager 管理生产密钥。
- 实现配置中心(如 Consul Config)远程拉取配置。
- 研究多租户场景的配置隔离策略。
- 学习 Feature Flags(功能开关)实现灰度发布。
适用场景
- 应用需要根据环境连接不同数据库。
- 需要记住用户的窗口位置、主题偏好。
- 需要支持命令行参数控制启动行为。
- 需要运行时修改配置无需重启应用。
落地建议
- 建立 appsettings.{env}.json 的标准模板。
- 封装 ISettingsService 接口统一管理配置读写。
- 在应用启动时验证必要配置项是否存在。
- 使用 IOptionsMonitor 实现配置热更新。
排错清单
- 检查 appsettings.json 是否设置为"复制到输出目录"。
- 确认环境变量名称与配置键的映射关系(前缀 + 双下划线分隔)。
- 检查 Properties.Settings.Save() 是否被调用。
- 确认 reloadOnChange 生效需要文件系统权限。
复盘问题
- 配置变更后是否需要重启应用才能生效?
- 用户升级应用后,Properties.Settings 是否能正确迁移?
- 如何在不暴露密钥的前提下实现 CI/CD 自动部署?
- 如何处理配置项缺失的优雅降级?
