Day 5 - 设置与偏好
建议用时:180-210 分钟
你将学会什么
- 设置和偏好有什么区别
- 为什么设置要有默认值
- 设置文件不存在时怎么办
- 设置值非法时怎么办
- 怎么保存到 JSON
- 怎么做加载、保存、恢复默认
设置不是随便存几个变量。一个可靠的设置功能至少要包含:默认值、加载、保存、非法值回退、恢复默认、保存路径明确。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“后台任务”,今天继续学习“设置与偏好”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 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() | 没文件时使用 |
| 保存 JSON | JsonSerializer.Serialize | 对象转文本 |
| 加载 JSON | JsonSerializer.Deserialize<AppSettings> | 文本转对象 |
| 设置目录 | Environment.GetFolderPath(...) | 应用数据目录 |
| 拼路径 | Path.Combine(...) | 跨平台 |
| 确保目录 | Directory.CreateDirectory(...) | 保存前创建 |
| 重置设置 | Reset() | 回到默认值 |
| 校验设置 | Normalize() | 防止非法值 |
设置保存流程:
ViewModel 修改设置属性
-> 点击保存
-> Normalize 校验
-> Repository 写 JSON
-> Message 显示成功或失败第二部分:把知识组合成完整例子
今天做一个 Avalonia 设置页面。
页面里有:
- 主题:
Light或Dark - 每页数量:例如
50 - 数据文件名:例如
products.json - 自动保存:勾选或不勾选
- 保存设置
- 重新加载
- 恢复默认
ViewModel 和设置仓库
文件位置:
ViewModels/MainWindowViewModel.csusing 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 步:运行后检查
运行后按顺序检查:
- 初次运行显示默认设置。
- 把主题改成
Dark,保存。 - 重新加载后仍然是
Dark。 - 把每页数量改成
abc,保存时提示必须是整数。 - 把每页数量改成
999,保存后回到50。 - 点击恢复默认,主题回到
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>验收结果
运行后必须看到:
- 设置文件不存在时显示默认值。
- 保存后生成
settings.json。 - 重新加载能恢复保存的设置。
- 页大小不是整数时不能保存。
- 页大小小于 10 或大于 500 时回到 50。
- 主题不是
Light或Dark时回到Light。 - 点击恢复默认后,界面回到默认值。
为什么这个答案是对的
这个答案覆盖了设置功能的完整闭环:
| 能力 | 代码 | 作用 |
|---|---|---|
| 默认值 | AppSettings | 第一次运行可用 |
| 保存 | Save | 写入 JSON |
| 加载 | Load | 从 JSON 恢复 |
| 回退 | Normalize | 修正非法设置 |
| 恢复默认 | Reset | 回到安全初始状态 |
| 页面绑定 | Theme、PageSizeText | 用户能修改 |
设置功能的关键是:文件不存在能用,文件损坏能恢复,输入非法能回退,保存路径明确。