Day 4 - 数据持久化
建议用时:180-210 分钟
你将学会什么
- 为什么程序关闭后内存数据会消失
- 什么是持久化
- 为什么 JSON 适合保存小型本地数据
- 为什么保存路径不要随便写在当前目录
- 怎么把保存和读取封装到
Repository - 怎么在 Avalonia 页面里保存和加载商品数据
本页目标不是只会写 JsonSerializer.Serialize。你要把完整流程连起来:界面输入商品、内存列表保存商品、Repository 写入 JSON、下次再从 JSON 读回来。
本页固定顺序
- 先学第一部分:弄懂今天最小、最重要的知识,并运行短例子。
- 再学第二部分:把刚学的知识组合成一个完整例子。
- 然后做第三部分:自己跟着敲,再完成重复训练和每日小测。
- 最后做第四部分:先独立完成作业,再用完整答案检查。
学习衔接
上一页学习的是“文件与对话框”,今天继续学习“数据持久化”。先使用上一页已经会的写法,再只增加今天这个新知识点;如果前置内容还不能独立敲出,先回上一页复习,不要硬跳。
今天的最低通过线
第一次学习不要求背完整页。完成下面 3 项,就可以继续:
- 能用自己的话说明“数据持久化”解决什么问题。
- 把第一部分的短例子亲手敲完,并确认每个例子都能运行。
- 不看完整答案完成第三部分至少前 3 个例子,再主动改一个值观察结果。
第一部分:先学原理和最小知识
这一部分从最小知识开始。先读解释,再把紧跟着的短例子敲一遍。持久化真正要理解的是:数据原来在哪里,保存后去了哪里,读取时又怎么回来。
为什么内存数据会丢
运行程序时,Products 这种集合存在内存里。
内存的特点是:程序开着时能用,程序关闭后就没了。
所以如果你只写:
Products.Add(new ProductItem { Name = "Keyboard", Price = 199m });这个商品只是在这次运行中存在。关闭程序再打开,它不会自动回来。
持久化就是把这些数据保存到更稳定的位置,例如文件或数据库。
什么是 JSON
JSON 是一种文本格式,常用来保存对象和列表。
一个商品对象可以保存成这样:
{
"Name": "Keyboard",
"Price": 199
}一个商品列表可以保存成这样:
[
{
"Name": "Keyboard",
"Price": 199
},
{
"Name": "Mouse",
"Price": 59
}
]JSON 的好处是:
- 人能直接打开文件看懂。
- C# 能方便地把对象转成 JSON。
- C# 也能把 JSON 再转回对象。
- 保存配置、小型列表、本地草稿都很合适。
序列化和反序列化
这两个词很重要。
| 名词 | 含义 | 本页代码 |
|---|---|---|
| 序列化 | C# 对象变成 JSON 文本 | JsonSerializer.Serialize(products) |
| 反序列化 | JSON 文本变回 C# 对象 | JsonSerializer.Deserialize<List<ProductItem>>(json) |
保存时走这个方向:
C# 商品列表 -> JSON 字符串 -> 写入文件读取时走这个方向:
读取文件 -> JSON 字符串 -> C# 商品列表为什么要封装 Repository
如果把保存和读取代码直接写在按钮里,代码很快会乱:
- 按钮里有校验。
- 按钮里有界面提示。
- 按钮里又有 JSON。
- 按钮里还要处理文件路径。
所以本页把文件读写放进 ProductRepository。
Repository 可以理解成“数据仓库入口”。界面和 ViewModel 不需要关心 JSON 文件怎么写,只调用:
await repository.SaveAsync(Products);
List<ProductItem> loadedProducts = await repository.LoadAsync();这样代码职责更清楚。
为什么保存到 ApplicationData
很多代码会直接写:
await File.WriteAllTextAsync("products.json", json);这在学习小例子里能跑,但做桌面程序时不稳定。
原因是:程序发布后,当前目录可能不是你以为的目录;有些目录也不允许写文件。
所以本页用:
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)它表示当前用户的应用数据目录。再拼上自己的应用文件夹:
Path.Combine(..., "ProductApp")这样保存位置更明确,也更适合桌面程序。
为什么读取时要处理三种情况
读取 JSON 时,至少要考虑三种情况:
- 文件还不存在。
- 文件是空的。
- JSON 内容损坏,不能转成对象。
本页先处理前两种:
if (!File.Exists(filePath))
{
return new List<ProductItem>();
}
if (string.IsNullOrWhiteSpace(json))
{
return new List<ProductItem>();
}第三种用 ViewModel 里的 try/catch 捕获,显示“读取失败”。
为什么 Deserialize 结果可能是 null
这行代码:
JsonSerializer.Deserialize<List<ProductItem>>(json)返回值类型是 List<ProductItem>?,意思是可能返回空值。
所以本页写成:
return JsonSerializer.Deserialize<List<ProductItem>>(json)
?? new List<ProductItem>();如果反序列化结果是空值,就返回空列表,避免后面代码崩掉。
JSON 持久化常用 API 速查
| 需求 | 写法 | 说明 |
|---|---|---|
| 对象转 JSON | JsonSerializer.Serialize(data, options) | 保存 |
| JSON 转对象 | JsonSerializer.Deserialize<T>(json) | 加载 |
| 美化 JSON | WriteIndented = true | 文件可读 |
| 读文件 | File.ReadAllText(path) | 加载文本 |
| 写文件 | File.WriteAllText(path, json) | 保存文本 |
| 判断文件存在 | File.Exists(path) | 第一次运行可能没有 |
| 创建目录 | Directory.CreateDirectory(path) | 保存前确保目录存在 |
| 拼路径 | Path.Combine(...) | 跨平台 |
持久化流程:
内存对象
-> Serialize 成 JSON
-> 写入文件
-> 下次启动读文件
-> Deserialize 回对象第二部分:把知识组合成完整例子
今天做一个“商品本地保存”页面。
页面上可以输入商品名和价格,点击“添加”放进列表。点击“保存 JSON”,把列表保存到本机文件。点击“读取 JSON”,再从文件恢复到列表。
文件结构
今天会用到 4 个文件:
Models/ProductItem.cs
Services/ProductRepository.cs
ViewModels/MainWindowViewModel.cs
Views/MainWindow.axaml数据模型
文件位置:
Models/ProductItem.csnamespace ProductApp.Models;
public sealed class ProductItem
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}JSON 保存和读取
文件位置:
Services/ProductRepository.csusing System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ProductApp.Models;
namespace ProductApp.Services;
public sealed class ProductRepository
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
private readonly string filePath;
public ProductRepository()
{
string folder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ProductApp");
Directory.CreateDirectory(folder);
filePath = Path.Combine(folder, "products.json");
}
public string FilePath => filePath;
public async Task SaveAsync(IEnumerable<ProductItem> products)
{
string json = JsonSerializer.Serialize(products, JsonOptions);
await File.WriteAllTextAsync(filePath, json);
}
public async Task<List<ProductItem>> LoadAsync()
{
if (!File.Exists(filePath))
{
return new List<ProductItem>();
}
string json = await File.ReadAllTextAsync(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return new List<ProductItem>();
}
return JsonSerializer.Deserialize<List<ProductItem>>(json)
?? new List<ProductItem>();
}
}ViewModel
文件位置:
ViewModels/MainWindowViewModel.csusing System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ProductApp.Models;
using ProductApp.Services;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly ProductRepository repository = new();
public ObservableCollection<ProductItem> Products { get; } = new();
[ObservableProperty]
private string name = "";
[ObservableProperty]
private string priceText = "";
[ObservableProperty]
private string message = "输入商品后点击添加。";
[RelayCommand]
private void AddProduct()
{
if (string.IsNullOrWhiteSpace(Name))
{
Message = "商品名不能为空。";
return;
}
if (!decimal.TryParse(PriceText, out decimal price) || price <= 0)
{
Message = "价格必须是大于 0 的数字。";
return;
}
Products.Add(new ProductItem
{
Name = Name.Trim(),
Price = price
});
Name = "";
PriceText = "";
Message = $"已添加商品,当前共 {Products.Count} 条。";
}
[RelayCommand]
private async Task SaveAsync()
{
try
{
await repository.SaveAsync(Products);
Message = $"已保存 {Products.Count} 条到 {repository.FilePath}";
}
catch (Exception ex)
{
Message = $"保存失败:{ex.Message}";
}
}
[RelayCommand]
private async Task LoadAsync()
{
try
{
List<ProductItem> loadedProducts = await repository.LoadAsync();
Products.Clear();
foreach (ProductItem product in loadedProducts)
{
Products.Add(product);
}
Message = $"已读取 {Products.Count} 条。";
}
catch (Exception ex)
{
Message = $"读取失败:{ex.Message}";
}
}
}窗口
文件位置:
Views/MainWindow.axaml<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
xmlns:models="using:ProductApp.Models"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="780"
Height="560"
Title="Json Persistence Demo">
<Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20" RowSpacing="12">
<TextBlock Text="商品本地保存"
FontSize="24"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Text="{Binding Message}"
Foreground="#555555" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<TextBox Width="180"
PlaceholderText="商品名"
Text="{Binding Name, Mode=TwoWay}" />
<TextBox Width="120"
PlaceholderText="价格"
Text="{Binding PriceText, Mode=TwoWay}" />
<Button Content="添加"
Command="{Binding AddProductCommand}" />
<Button Content="保存 JSON"
Command="{Binding SaveCommand}" />
<Button Content="读取 JSON"
Command="{Binding LoadCommand}" />
</StackPanel>
<ListBox Grid.Row="3"
ItemsSource="{Binding Products}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:ProductItem">
<Grid ColumnDefinitions="200,120" Margin="4">
<TextBlock Text="{Binding Name}" />
<TextBlock Grid.Column="1"
Text="{Binding Price}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>第三部分:跟着敲代码
从这里开始动手。按文件顺序写,不要先写界面。
第 1 步:创建 Models 文件夹和 ProductItem
新建文件:
Models/ProductItem.cs写入:
namespace ProductApp.Models;
public sealed class ProductItem
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}解释:
- 这个类只表示要保存的数据。
Name和Price要有get; set;,JSON 才能正常写入和读取。
第 2 步:创建 Services 文件夹和 Repository
新建文件:
Services/ProductRepository.cs先写类结构和保存路径。
public sealed class ProductRepository
{
private readonly string filePath;
public ProductRepository()
{
string folder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ProductApp");
Directory.CreateDirectory(folder);
filePath = Path.Combine(folder, "products.json");
}
public string FilePath => filePath;
}解释:
filePath保存 JSON 文件完整位置。Directory.CreateDirectory(folder)确保文件夹存在。FilePath暴露出来,是为了界面提示保存到了哪里。
第 3 步:写保存方法
继续在 ProductRepository 里写:
public async Task SaveAsync(IEnumerable<ProductItem> products)
{
string json = JsonSerializer.Serialize(products, JsonOptions);
await File.WriteAllTextAsync(filePath, json);
}解释:
IEnumerable<ProductItem>表示只要是商品集合就能保存。Serialize把商品集合转成 JSON。WriteAllTextAsync把 JSON 写入文件。
第 4 步:写读取方法
继续写:
public async Task<List<ProductItem>> LoadAsync()
{
if (!File.Exists(filePath))
{
return new List<ProductItem>();
}
string json = await File.ReadAllTextAsync(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return new List<ProductItem>();
}
return JsonSerializer.Deserialize<List<ProductItem>>(json)
?? new List<ProductItem>();
}解释:
- 没有文件时返回空列表。
- 文件为空时返回空列表。
- 有内容时,把 JSON 转回商品列表。
第 5 步:ViewModel 里准备输入和列表
打开:
ViewModels/MainWindowViewModel.cs加入这些属性:
private readonly ProductRepository repository = new();
public ObservableCollection<ProductItem> Products { get; } = new();
[ObservableProperty]
private string name = "";
[ObservableProperty]
private string priceText = "";
[ObservableProperty]
private string message = "输入商品后点击添加。";解释:
Products是界面正在显示的商品列表。Name绑定商品名输入框。PriceText绑定价格输入框。Message显示添加、保存、读取结果。
第 6 步:写添加商品命令
[RelayCommand]
private void AddProduct()
{
if (string.IsNullOrWhiteSpace(Name))
{
Message = "商品名不能为空。";
return;
}
if (!decimal.TryParse(PriceText, out decimal price) || price <= 0)
{
Message = "价格必须是大于 0 的数字。";
return;
}
Products.Add(new ProductItem
{
Name = Name.Trim(),
Price = price
});
Name = "";
PriceText = "";
Message = $"已添加商品,当前共 {Products.Count} 条。";
}解释:
- 商品名为空时不添加。
- 价格不是数字时不添加。
- 添加成功后清空输入框。
[RelayCommand]会生成AddProductCommand。
第 7 步:写保存命令
[RelayCommand]
private async Task SaveAsync()
{
try
{
await repository.SaveAsync(Products);
Message = $"已保存 {Products.Count} 条到 {repository.FilePath}";
}
catch (Exception ex)
{
Message = $"保存失败:{ex.Message}";
}
}解释:
SaveAsync调用 Repository。- 成功后显示保存条数和文件路径。
- 失败时显示失败原因。
[RelayCommand]会生成SaveCommand。
第 8 步:写读取命令
[RelayCommand]
private async Task LoadAsync()
{
try
{
List<ProductItem> loadedProducts = await repository.LoadAsync();
Products.Clear();
foreach (ProductItem product in loadedProducts)
{
Products.Add(product);
}
Message = $"已读取 {Products.Count} 条。";
}
catch (Exception ex)
{
Message = $"读取失败:{ex.Message}";
}
}解释:
- 先从 JSON 读取商品列表。
- 再清空当前界面列表。
- 最后把读取到的数据一条一条加入
Products。 [RelayCommand]会生成LoadCommand。
第 9 步:写窗口绑定
打开:
Views/MainWindow.axaml先写输入区:
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<TextBox Width="180"
PlaceholderText="商品名"
Text="{Binding Name, Mode=TwoWay}" />
<TextBox Width="120"
PlaceholderText="价格"
Text="{Binding PriceText, Mode=TwoWay}" />
<Button Content="添加"
Command="{Binding AddProductCommand}" />
<Button Content="保存 JSON"
Command="{Binding SaveCommand}" />
<Button Content="读取 JSON"
Command="{Binding LoadCommand}" />
</StackPanel>再写列表:
<ListBox Grid.Row="3"
ItemsSource="{Binding Products}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:ProductItem">
<Grid ColumnDefinitions="200,120" Margin="4">
<TextBlock Text="{Binding Name}" />
<TextBlock Grid.Column="1"
Text="{Binding Price}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>解释:
- 输入框绑定 ViewModel 的属性。
- 按钮绑定 ViewModel 的命令。
- 列表绑定
Products。 - 每一行显示
Name和Price。
第 10 步:运行后检查
按这个顺序检查:
- 输入
Keyboard和199,点击添加。 - 输入
Mouse和59,点击添加。 - 点击“保存 JSON”。
- 关闭程序。
- 重新启动程序。
- 点击“读取 JSON”。
- 列表恢复出
Keyboard和Mouse。 - 输入空商品名,应该提示商品名不能为空。
- 输入价格
abc,应该提示价格必须是数字。
常见错误和修法
| 错误 | 为什么错 | 修法 |
|---|---|---|
| 程序启动不加载数据 | 之前保存的数据看不到 | 启动时调用加载方法 |
| 保存时覆盖错误内容 | 数据可能丢失 | 保存前确认序列化结果和路径 |
| JSON 文件为空时报错 | 空文件不能直接反序列化 | 空白时返回默认列表 |
| 数据模型改了旧文件读不了 | 版本变化导致字段不匹配 | 给字段默认值,必要时写迁移逻辑 |
| 保存失败没有提示 | 用户以为保存成功 | 捕获异常并显示错误消息 |
小白重复敲写训练
持久化先保存一条对象,再保存集合。
训练 1:对象转 JSON
var product = new Product { Name = "Keyboard", Price = 199m };
string json = JsonSerializer.Serialize(product, new JsonSerializerOptions
{
WriteIndented = true
});
Console.WriteLine(json);训练 2:JSON 保存到文件
await File.WriteAllTextAsync("product.json", json);
string saved = await File.ReadAllTextAsync("product.json");
Console.WriteLine(saved);训练 3:从 JSON 恢复对象
Product? loaded = JsonSerializer.Deserialize<Product>(saved);
if (loaded is not null)
{
Console.WriteLine($"{loaded.Name}: {loaded.Price:F2}");
}第三遍改成保存 List<Product>,至少放两条数据。
每日小测
做完本页后,用这 5 题检查是否真的掌握。
1. 判断题
本页的目标不是只把代码运行起来,还要能说清楚“为什么这样写”。
答案:对。能运行只是第一步,能解释原理、常用操作和常见错误,才说明本页内容进入了可复用能力。
2. 填空题
本页主题是:数据持久化。今天至少要掌握的 3 个点是:
1. 为什么程序关闭后内存数据会消失
2. 什么是持久化
3. 为什么 JSON 适合保存小型本地数据答案:以上 3 点必须能用自己的代码跑通,不能只停留在阅读。
3. 流程题
遇到本页相关功能时,先按什么顺序处理?
答案:先看完整例子,确认最终效果;再读原理和名词;然后跟着第三部分从空项目敲代码;最后对照作业答案检查。
4. 找错误题
如果本页代码运行失败,第一步应该做什么?
答案:先看终端或 IDE 里的第一条错误,找到文件名和行号;不要同时改很多地方。再回到本页的“常见错误和修法”表格,对照错误类型逐项排查。
5. 改需求题
在本页完整例子跑通后,至少改一个小需求。
可选改法:
- 改一个字段名称。
- 多加一个校验条件。
- 多输出一行结果。
- 把固定数据改成用户输入。
- 把一次处理改成多条数据处理。
答案标准:修改后能重新运行,并能说明这次修改影响了哪一段逻辑。重点检查:为什么程序关闭后内存数据会消失。
上位机专项练习
JSON 用来保存设备配置和界面设置,启动时读取,修改后写回。
下面 3 个例子都要亲手敲。先运行原代码,再完成每个例子后面的改动任务。
专项例子 1:保存设备配置
var config = new DeviceConfig("PLC-01", "192.168.1.10", 502);
string json = JsonSerializer.Serialize(config, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync("device.json", json);运行结果或界面效果:
生成可读的 device.json改动任务: 增加 PollInterval 属性。
专项例子 2:读取设备配置
string json = await File.ReadAllTextAsync("device.json");
DeviceConfig? config = JsonSerializer.Deserialize<DeviceConfig>(json);
Console.WriteLine($"{config?.Name}: {config?.IpAddress}:{config?.Port}");运行结果或界面效果:
PLC-01: 192.168.1.10:502改动任务: 文件不存在时使用默认配置。
专项例子 3:保存最近使用设置
var settings = new AppSettings
{
LastDeviceId = "PLC-01",
Theme = "Light",
WindowWidth = 1200
};运行结果或界面效果:
下次启动可恢复设备、主题和窗口宽度改动任务: 增加 WindowHeight。
第四部分:作业完整答案
作业要求:做一个商品本地保存页面,能添加商品、保存 JSON、读取 JSON,并处理空名称、错误价格、文件不存在、读取失败。
答案文件 1:Models/ProductItem.cs
namespace ProductApp.Models;
public sealed class ProductItem
{
public string Name { get; set; } = "";
public decimal Price { get; set; }
}答案文件 2:Services/ProductRepository.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using ProductApp.Models;
namespace ProductApp.Services;
public sealed class ProductRepository
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true
};
private readonly string filePath;
public ProductRepository()
{
string folder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"ProductApp");
Directory.CreateDirectory(folder);
filePath = Path.Combine(folder, "products.json");
}
public string FilePath => filePath;
public async Task SaveAsync(IEnumerable<ProductItem> products)
{
string json = JsonSerializer.Serialize(products, JsonOptions);
await File.WriteAllTextAsync(filePath, json);
}
public async Task<List<ProductItem>> LoadAsync()
{
if (!File.Exists(filePath))
{
return new List<ProductItem>();
}
string json = await File.ReadAllTextAsync(filePath);
if (string.IsNullOrWhiteSpace(json))
{
return new List<ProductItem>();
}
return JsonSerializer.Deserialize<List<ProductItem>>(json)
?? new List<ProductItem>();
}
}答案文件 3:ViewModels/MainWindowViewModel.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ProductApp.Models;
using ProductApp.Services;
namespace ProductApp.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly ProductRepository repository = new();
public ObservableCollection<ProductItem> Products { get; } = new();
[ObservableProperty]
private string name = "";
[ObservableProperty]
private string priceText = "";
[ObservableProperty]
private string message = "输入商品后点击添加。";
[RelayCommand]
private void AddProduct()
{
if (string.IsNullOrWhiteSpace(Name))
{
Message = "商品名不能为空。";
return;
}
if (!decimal.TryParse(PriceText, out decimal price) || price <= 0)
{
Message = "价格必须是大于 0 的数字。";
return;
}
Products.Add(new ProductItem
{
Name = Name.Trim(),
Price = price
});
Name = "";
PriceText = "";
Message = $"已添加商品,当前共 {Products.Count} 条。";
}
[RelayCommand]
private async Task SaveAsync()
{
try
{
await repository.SaveAsync(Products);
Message = $"已保存 {Products.Count} 条到 {repository.FilePath}";
}
catch (Exception ex)
{
Message = $"保存失败:{ex.Message}";
}
}
[RelayCommand]
private async Task LoadAsync()
{
try
{
List<ProductItem> loadedProducts = await repository.LoadAsync();
Products.Clear();
foreach (ProductItem product in loadedProducts)
{
Products.Add(product);
}
Message = $"已读取 {Products.Count} 条。";
}
catch (Exception ex)
{
Message = $"读取失败:{ex.Message}";
}
}
}答案文件 4:Views/MainWindow.axaml
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ProductApp.ViewModels"
xmlns:models="using:ProductApp.Models"
x:Class="ProductApp.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Width="780"
Height="560"
Title="Json Persistence Demo">
<Grid RowDefinitions="Auto,Auto,Auto,*" Margin="20" RowSpacing="12">
<TextBlock Text="商品本地保存"
FontSize="24"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Text="{Binding Message}"
Foreground="#555555" />
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="8">
<TextBox Width="180"
PlaceholderText="商品名"
Text="{Binding Name, Mode=TwoWay}" />
<TextBox Width="120"
PlaceholderText="价格"
Text="{Binding PriceText, Mode=TwoWay}" />
<Button Content="添加"
Command="{Binding AddProductCommand}" />
<Button Content="保存 JSON"
Command="{Binding SaveCommand}" />
<Button Content="读取 JSON"
Command="{Binding LoadCommand}" />
</StackPanel>
<ListBox Grid.Row="3"
ItemsSource="{Binding Products}">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="models:ProductItem">
<Grid ColumnDefinitions="200,120" Margin="4">
<TextBlock Text="{Binding Name}" />
<TextBlock Grid.Column="1"
Text="{Binding Price}" />
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Window>验收结果
运行后必须看到这些结果:
- 商品名为空时不能添加。
- 价格不是数字时不能添加。
- 正常商品能显示在列表里。
- 点击“保存 JSON”后提示保存路径。
- 关闭程序再打开,点击“读取 JSON”能恢复列表。
- 第一次运行时,即使文件不存在,点击读取也不会崩溃。
为什么这个答案是对的
这个答案把持久化拆成了明确的四层:
| 层级 | 代码 | 作用 |
|---|---|---|
| 数据模型 | ProductItem | 决定 JSON 里有哪些字段 |
| 文件仓库 | ProductRepository | 负责 JSON 保存和读取 |
| 页面状态 | MainWindowViewModel | 负责输入、校验、命令、提示 |
| 界面显示 | MainWindow.axaml | 负责输入框、按钮、列表 |
关闭程序后,Products 里的内存数据会消失;保存到 JSON 后,数据写到了磁盘文件;下次读取 JSON,就能把磁盘文件恢复成内存里的商品列表。