Skip to Content
Week 11Day 5 - 设置与偏好

Day 5 - 设置与偏好

建议用时:180-210 分钟

你将学会什么

  • 设置和偏好有什么区别
  • 为什么设置要有默认值
  • 设置文件不存在时怎么办
  • 设置值非法时怎么办
  • 怎么保存到 JSON
  • 怎么做加载、保存、恢复默认

设置不是随便存几个变量。一个可靠的设置功能至少要包含:默认值、加载、保存、非法值回退、恢复默认、保存路径明确。

本页固定顺序

  1. 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
  2. 再学第二部分:把刚学的知识组合成一个完整例子。
  3. 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
  4. 最后做第四部分:先独立完成作业,再用完整答案检查。

学习衔接

上一页学习的是“后台任务”,今天继续学习“设置与偏好”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。

今天的最低通过线

第一次学习不要求背完整页。完成下面 3 项,就可以继续:

  • 能用自己的话说明“设置与偏好”解决什么问题。
  • 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
  • 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。

第一部分:先学原理和最小知识

这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。设置功能最重要的是默认值和错误恢复。

设置和偏好的区别

名词含义例子
设置影响程序行为的选项每页显示多少条、数据文件路径
偏好用户个人习惯主题、窗口大小、自动保存

两者都需要保存,但重要程度不同。

为什么要有默认值

第一次运行程序时,设置文件不存在。

这时程序不能崩溃,应该使用默认值:

public string Theme { get; set; } = "Light"; public int PageSize { get; set; } = 50;

默认值就是程序的安全起点。

设置文件放在哪里

不要把设置写在源码目录。

本页保存到:

Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)

然后放到:

ProductApp/settings.json

这样发布后也能稳定保存。

为什么要 Normalize

设置文件可能被手动改坏。

例如:

{ "Theme": "Blue", "PageSize": -1, "DataPath": "" }

这些值都不合法。

所以加载后要统一修正:

private static AppSettings Normalize(AppSettings settings)

非法主题回到 Light

非法页大小回到 50

空路径回到 products.json

为什么捕获读取失败

如果 settings.json 损坏,JsonSerializer.Deserialize 可能失败。

本页写:

catch { return new AppSettings(); }

意思是:设置坏了,就回到默认设置,程序继续运行。

不要把敏感密钥当普通设置保存

普通设置可以保存主题、页大小、路径。

不要直接保存:

  • 密码
  • Token
  • Secret
  • 私人证件号

这类内容需要更严格的安全处理,不能当普通偏好写入 JSON。

设置与偏好常用操作速查

需求写法说明
设置模型AppSettings保存偏好字段
默认设置new AppSettings()没文件时使用
保存 JSONJsonSerializer.Serialize对象转文本
加载 JSONJsonSerializer.Deserialize<AppSettings>文本转对象
设置目录Environment.GetFolderPath(...)应用数据目录
拼路径Path.Combine(...)跨平台
确保目录Directory.CreateDirectory(...)保存前创建
重置设置Reset()回到默认值
校验设置Normalize()防止非法值

设置保存流程:

ViewModel 修改设置属性 -> 点击保存 -> Normalize 校验 -> Repository 写 JSON -> Message 显示成功或失败

第二部分:把知识组合成完整例子

今天做一个 Avalonia 设置页面。

页面里有:

  • 主题:LightDark
  • 每页数量:例如 50
  • 数据文件名:例如 products.json
  • 自动保存:勾选或不勾选
  • 保存设置
  • 重新加载
  • 恢复默认

ViewModel 和设置仓库

文件位置:

ViewModels/MainWindowViewModel.cs
using System; using System.IO; using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { private readonly SettingsRepository repository = new(); [ObservableProperty] private string theme = "Light"; [ObservableProperty] private string pageSizeText = "50"; [ObservableProperty] private string dataPath = "products.json"; [ObservableProperty] private bool autoSave = true; [ObservableProperty] private string message = ""; public MainWindowViewModel() { LoadFromFile(); } [RelayCommand] private void SaveSettings() { if (!int.TryParse(PageSizeText, out int pageSize)) { Message = "每页数量必须是整数。"; return; } var settings = new AppSettings { Theme = Theme, PageSize = pageSize, DataPath = DataPath, AutoSave = AutoSave }; repository.Save(settings); ApplySettings(repository.Load()); Message = $"设置已保存:{repository.FilePath}"; } [RelayCommand] private void LoadSettings() { LoadFromFile(); Message = $"设置已加载:{repository.FilePath}"; } [RelayCommand] private void ResetSettings() { AppSettings settings = repository.Reset(); ApplySettings(settings); Message = "已恢复默认设置。"; } private void LoadFromFile() { AppSettings settings = repository.Load(); ApplySettings(settings); Message = $"当前设置文件:{repository.FilePath}"; } private void ApplySettings(AppSettings settings) { Theme = settings.Theme; PageSizeText = settings.PageSize.ToString(); DataPath = settings.DataPath; AutoSave = settings.AutoSave; } } public sealed class AppSettings { public string Theme { get; set; } = "Light"; public int PageSize { get; set; } = 50; public string DataPath { get; set; } = "products.json"; public bool AutoSave { get; set; } = true; } public sealed class SettingsRepository { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private readonly string filePath; public SettingsRepository() { string folder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ProductApp"); Directory.CreateDirectory(folder); filePath = Path.Combine(folder, "settings.json"); } public string FilePath => filePath; public AppSettings Load() { if (!File.Exists(filePath)) { return new AppSettings(); } try { string json = File.ReadAllText(filePath); AppSettings settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings(); return Normalize(settings); } catch { return new AppSettings(); } } public void Save(AppSettings settings) { AppSettings normalized = Normalize(settings); string json = JsonSerializer.Serialize(normalized, JsonOptions); File.WriteAllText(filePath, json); } public AppSettings Reset() { var settings = new AppSettings(); Save(settings); return settings; } private static AppSettings Normalize(AppSettings settings) { if (settings.Theme is not "Light" and not "Dark") { settings.Theme = "Light"; } if (settings.PageSize < 10 || settings.PageSize > 500) { settings.PageSize = 50; } if (string.IsNullOrWhiteSpace(settings.DataPath)) { settings.DataPath = "products.json"; } return settings; } }

窗口

文件位置:

Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="680" Height="460" Title="Settings"> <StackPanel Margin="20" Spacing="10"> <TextBlock Text="设置与偏好" FontSize="24" FontWeight="Bold" /> <TextBlock Text="{Binding Message}" /> <TextBlock Text="主题:Light 或 Dark" /> <TextBox Text="{Binding Theme, Mode=TwoWay}" /> <TextBlock Text="每页数量:10 到 500" /> <TextBox Text="{Binding PageSizeText, Mode=TwoWay}" /> <TextBlock Text="数据文件名" /> <TextBox Text="{Binding DataPath, Mode=TwoWay}" /> <CheckBox Content="自动保存" IsChecked="{Binding AutoSave, Mode=TwoWay}" /> <StackPanel Orientation="Horizontal" Spacing="8"> <Button Content="保存设置" Command="{Binding SaveSettingsCommand}" /> <Button Content="重新加载" Command="{Binding LoadSettingsCommand}" /> <Button Content="恢复默认" Command="{Binding ResetSettingsCommand}" /> </StackPanel> </StackPanel> </Window>

第三部分:跟着敲代码

从这里开始动手。打开 Avalonia 项目。

第 1 步:写 AppSettings

public sealed class AppSettings { public string Theme { get; set; } = "Light"; public int PageSize { get; set; } = 50; public string DataPath { get; set; } = "products.json"; public bool AutoSave { get; set; } = true; }

解释:

  • 每个属性都有默认值。
  • JSON 读取失败时可以直接回到默认设置。

第 2 步:写 SettingsRepository 构造函数

private readonly string filePath; public SettingsRepository() { string folder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ProductApp"); Directory.CreateDirectory(folder); filePath = Path.Combine(folder, "settings.json"); } public string FilePath => filePath;

解释:

  • 设置文件路径集中管理。
  • FilePath 用来显示当前设置保存位置。

第 3 步:写 Load

public AppSettings Load() { if (!File.Exists(filePath)) { return new AppSettings(); } try { string json = File.ReadAllText(filePath); AppSettings settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings(); return Normalize(settings); } catch { return new AppSettings(); } }

解释:

  • 文件不存在,返回默认设置。
  • 文件损坏,也返回默认设置。
  • 文件存在且能读,再做合法化处理。

第 4 步:写 Save 和 Reset

public void Save(AppSettings settings) { AppSettings normalized = Normalize(settings); string json = JsonSerializer.Serialize(normalized, JsonOptions); File.WriteAllText(filePath, json); } public AppSettings Reset() { var settings = new AppSettings(); Save(settings); return settings; }

解释:

  • 保存前也做 Normalize。
  • 恢复默认就是保存一份默认设置。

第 5 步:写 Normalize

private static AppSettings Normalize(AppSettings settings) { if (settings.Theme is not "Light" and not "Dark") { settings.Theme = "Light"; } if (settings.PageSize < 10 || settings.PageSize > 500) { settings.PageSize = 50; } if (string.IsNullOrWhiteSpace(settings.DataPath)) { settings.DataPath = "products.json"; } return settings; }

解释:

  • 设置值不合法时回到安全默认值。

第 6 步:写 ViewModel 字段

[ObservableProperty] private string theme = "Light"; [ObservableProperty] private string pageSizeText = "50"; [ObservableProperty] private string dataPath = "products.json"; [ObservableProperty] private bool autoSave = true; [ObservableProperty] private string message = "";

解释:

  • 输入框里的页大小是文本,所以用 PageSizeText

第 7 步:写保存命令

[RelayCommand] private void SaveSettings() { if (!int.TryParse(PageSizeText, out int pageSize)) { Message = "每页数量必须是整数。"; return; } var settings = new AppSettings { Theme = Theme, PageSize = pageSize, DataPath = DataPath, AutoSave = AutoSave }; repository.Save(settings); ApplySettings(repository.Load()); Message = $"设置已保存:{repository.FilePath}"; }

解释:

  • 先检查页大小能不能转成整数。
  • 保存后再加载一次,让 Normalize 后的值回到界面。

第 8 步:写加载和恢复默认

[RelayCommand] private void LoadSettings() { LoadFromFile(); Message = $"设置已加载:{repository.FilePath}"; } [RelayCommand] private void ResetSettings() { AppSettings settings = repository.Reset(); ApplySettings(settings); Message = "已恢复默认设置。"; }

解释:

  • 重新加载从文件读。
  • 恢复默认写入默认设置。

第 9 步:写设置页面 XAML

<TextBlock Text="主题:Light 或 Dark" /> <TextBox Text="{Binding Theme, Mode=TwoWay}" /> <TextBlock Text="每页数量:10 到 500" /> <TextBox Text="{Binding PageSizeText, Mode=TwoWay}" /> <TextBlock Text="数据文件名" /> <TextBox Text="{Binding DataPath, Mode=TwoWay}" /> <CheckBox Content="自动保存" IsChecked="{Binding AutoSave, Mode=TwoWay}" />

解释:

  • 输入框绑定 ViewModel 字段。
  • 复选框绑定布尔值。

第 10 步:运行后检查

运行后按顺序检查:

  1. 初次运行显示默认设置。
  2. 把主题改成 Dark,保存。
  3. 重新加载后仍然是 Dark
  4. 把每页数量改成 abc,保存时提示必须是整数。
  5. 把每页数量改成 999,保存后回到 50
  6. 点击恢复默认,主题回到 Light

常见错误和修法

错误为什么错修法
设置值写死在代码里用户无法修改,发布后不灵活保存到配置文件或本地设置
设置修改后不生效只改了变量,没有通知界面或服务保存后刷新相关状态
配置文件不存在就报错第一次运行常常没有配置不存在时创建默认设置
设置项没有默认值读取失败后程序状态不确定给每个设置定义明确默认值
偏好设置混入业务规则设置页会变复杂设置只保存偏好,业务规则仍放 Service

小白重复敲写训练

设置功能先保存一个字段,再逐步增加。

训练 1:设置对象

public class AppSettings { public string Theme { get; set; } = "Light"; public int PageSize { get; set; } = 20; }

训练 2:保存设置

var settings = new AppSettings { Theme = "Dark", PageSize = 50 }; string json = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true }); await File.WriteAllTextAsync("settings.json", json);

训练 3:文件不存在时使用默认值

AppSettings settings; if (File.Exists("settings.json")) { string json = await File.ReadAllTextAsync("settings.json"); settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new(); } else { settings = new AppSettings(); }

第三遍保存后重新启动程序验证设置仍存在。

每日小测

做完本页后,用这 5 题检查是否真的掌握。

1. 判断题

本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。

答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。

2. 填空题

本页主题是:设置与偏好。今天至少要掌握的 3 个点是:

1. 设置和偏好有什么区别 2. 为什么设置要有默认值 3. 设置文件不存在时怎么办

答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。

3. 流程题

遇到本页相关功能时,先按什么顺序处理?

答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。

4. 找错误题

如果本页代码运行失败,第一步应该做什么?

答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。

5. 改需求题

在本页完整例子跑通后,至少改一个小需求。

可选改法:

  • 改一个字段名称。
  • 多加一个校验条件。
  • 多输出一行结果。
  • 把固定数据改成用户输入。
  • 把一次处理改成多条数据处理。

答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:设置和偏好有什么区别。

上位机专项练习

设置页集中管理通信、采集、日志和主题,并区分立即生效与重启后生效。

下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。

专项例子 1:采集设置

<StackPanel Spacing="10"> <NumericUpDown Value="{Binding PollInterval}" Minimum="200" Maximum="60000" /> <NumericUpDown Value="{Binding ConnectionTimeout}" Minimum="500" Maximum="30000" /> <CheckBox Content="启动时自动连接" IsChecked="{Binding AutoConnect}" /> </StackPanel>

运行结果或界面效果:

可以配置周期、超时和自动连接

改动任务: 为每项增加单位文字。

专项例子 2:保存设置

[RelayCommand] private async Task SaveSettingsAsync() { await settingsStore.SaveAsync(Settings); Message = "设置已保存,采集周期立即生效"; }

运行结果或界面效果:

保存后明确告诉用户生效方式

改动任务: 修改端口时提示需要重新连接。

专项例子 3:恢复默认设置

[RelayCommand] private void ResetDefaults() { PollInterval = 1000; ConnectionTimeout = 3000; AutoConnect = false; }

运行结果或界面效果:

一键恢复安全默认值

改动任务: 恢复前增加确认对话框。

第四部分:作业完整答案

作业要求:做一个设置页面,支持加载、保存、恢复默认,并把设置保存为 JSON。

答案文件 1:ViewModels/MainWindowViewModel.cs

using System; using System.IO; using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; namespace ProductApp.ViewModels; public partial class MainWindowViewModel : ViewModelBase { private readonly SettingsRepository repository = new(); [ObservableProperty] private string theme = "Light"; [ObservableProperty] private string pageSizeText = "50"; [ObservableProperty] private string dataPath = "products.json"; [ObservableProperty] private bool autoSave = true; [ObservableProperty] private string message = ""; public MainWindowViewModel() { LoadFromFile(); } [RelayCommand] private void SaveSettings() { if (!int.TryParse(PageSizeText, out int pageSize)) { Message = "每页数量必须是整数。"; return; } var settings = new AppSettings { Theme = Theme, PageSize = pageSize, DataPath = DataPath, AutoSave = AutoSave }; repository.Save(settings); ApplySettings(repository.Load()); Message = $"设置已保存:{repository.FilePath}"; } [RelayCommand] private void LoadSettings() { LoadFromFile(); Message = $"设置已加载:{repository.FilePath}"; } [RelayCommand] private void ResetSettings() { AppSettings settings = repository.Reset(); ApplySettings(settings); Message = "已恢复默认设置。"; } private void LoadFromFile() { AppSettings settings = repository.Load(); ApplySettings(settings); Message = $"当前设置文件:{repository.FilePath}"; } private void ApplySettings(AppSettings settings) { Theme = settings.Theme; PageSizeText = settings.PageSize.ToString(); DataPath = settings.DataPath; AutoSave = settings.AutoSave; } } public sealed class AppSettings { public string Theme { get; set; } = "Light"; public int PageSize { get; set; } = 50; public string DataPath { get; set; } = "products.json"; public bool AutoSave { get; set; } = true; } public sealed class SettingsRepository { private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true }; private readonly string filePath; public SettingsRepository() { string folder = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ProductApp"); Directory.CreateDirectory(folder); filePath = Path.Combine(folder, "settings.json"); } public string FilePath => filePath; public AppSettings Load() { if (!File.Exists(filePath)) { return new AppSettings(); } try { string json = File.ReadAllText(filePath); AppSettings settings = JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings(); return Normalize(settings); } catch { return new AppSettings(); } } public void Save(AppSettings settings) { AppSettings normalized = Normalize(settings); string json = JsonSerializer.Serialize(normalized, JsonOptions); File.WriteAllText(filePath, json); } public AppSettings Reset() { var settings = new AppSettings(); Save(settings); return settings; } private static AppSettings Normalize(AppSettings settings) { if (settings.Theme is not "Light" and not "Dark") { settings.Theme = "Light"; } if (settings.PageSize < 10 || settings.PageSize > 500) { settings.PageSize = 50; } if (string.IsNullOrWhiteSpace(settings.DataPath)) { settings.DataPath = "products.json"; } return settings; } }

答案文件 2:Views/MainWindow.axaml

<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="using:ProductApp.ViewModels" x:Class="ProductApp.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Width="680" Height="460" Title="Settings"> <StackPanel Margin="20" Spacing="10"> <TextBlock Text="设置与偏好" FontSize="24" FontWeight="Bold" /> <TextBlock Text="{Binding Message}" /> <TextBlock Text="主题:Light 或 Dark" /> <TextBox Text="{Binding Theme, Mode=TwoWay}" /> <TextBlock Text="每页数量:10 到 500" /> <TextBox Text="{Binding PageSizeText, Mode=TwoWay}" /> <TextBlock Text="数据文件名" /> <TextBox Text="{Binding DataPath, Mode=TwoWay}" /> <CheckBox Content="自动保存" IsChecked="{Binding AutoSave, Mode=TwoWay}" /> <StackPanel Orientation="Horizontal" Spacing="8"> <Button Content="保存设置" Command="{Binding SaveSettingsCommand}" /> <Button Content="重新加载" Command="{Binding LoadSettingsCommand}" /> <Button Content="恢复默认" Command="{Binding ResetSettingsCommand}" /> </StackPanel> </StackPanel> </Window>

验收结果

运行后必须看到:

  1. 设置文件不存在时显示默认值。
  2. 保存后生成 settings.json
  3. 重新加载能恢复保存的设置。
  4. 页大小不是整数时不能保存。
  5. 页大小小于 10 或大于 500 时回到 50。
  6. 主题不是 LightDark 时回到 Light
  7. 点击恢复默认后,界面回到默认值。

为什么这个答案是对的

这个答案覆盖了设置功能的完整闭环:

能力代码作用
默认值AppSettings第一次运行可用
保存Save写入 JSON
加载Load从 JSON 恢复
回退Normalize修正非法设置
恢复默认Reset回到安全初始状态
页面绑定ThemePageSizeText用户能修改

设置功能的关键是:文件不存在能用,文件损坏能恢复,输入非法能回退,保存路径明确。