.NET MAUI本地保存加载特定用户数据的实现方案咨询
我正在开发一款.NET MAUI应用,需要在设备本地保存用户数据,要求应用启动时检查是否存在已保存数据,若有则加载至应用中展示和操作。
数据结构不复杂,包含存储DateTime对象的集合、若干字符串及整数,我尝试用JSON序列化,将数据加载至UserData类,该类代码如下:
public partial class UserData : ObservableObject { public UserData() { } [ObservableProperty] private ObservableCollection<DateTime> takeOffs = new(); [ObservableProperty] private ObservableCollection<DateTime> landings = new(); [ObservableProperty] private ObservableCollection<RecencyItem> recencyData = new(); [ObservableProperty] private DateTime? ldgExpiration; [ObservableProperty] private DateTime? takeOffExpiration; [ObservableProperty] private bool isRecent = false; [ObservableProperty] private string recencyText = "NOT ENOUGH DATA"; }
我遵循MVVM模式,在MauiProgram.cs中实例化类:
builder.Services .AddSingleton<UserData>() .AddSingleton<MainPage>() .AddSingleton<MainViewModel>() .AddSingleton<TakeOffsPage>() .AddSingleton<TakeoffsViewModel>() .AddSingleton<LandingsPage>() .AddSingleton<LandingsViewModel>() .AddTransient<AddActivityPage>() .AddTransient<AddActivityViewModel>() .AddSingleton<CertificatesPage>() .AddSingleton<CertificatesViewModel>() .AddTransient<AddCertificatesPages>() .AddTransient<AddCertificatesViewModel>();
JSON序列化与反序列化代码:
public static UserData GetData(string jsonFilePath) { try { if (File.Exists(jsonFilePath)) { using var reader = new StreamReader(jsonFilePath); string data = reader.ReadToEnd(); return JsonSerializer.Deserialize<UserData>(data); } else return null; } catch (Exception ex) { Debug.WriteLine($"{ex.Message}"); return null; } } public static void SaveData(string jsonFilePath, UserData userData) { try { string data = JsonSerializer.Serialize(userData); using var writer = new StreamWriter(jsonFilePath); writer.Write(data); } catch (Exception ex) { Debug.WriteLine($"{ex.Message}"); } }
目前能成功保存数据到JSON文件,但无法正确加载,加载代码如下:
public MainViewModel(UserData userData) { this.UserData = userData; LoadUserData(JSON_FILE_NAME); } public void LoadUserData(string fileName) { try { string jsonFilePath = Path.Combine(FileSystem.AppDataDirectory, fileName); UserData loadedUserData = JsonService.GetData(jsonFilePath); if (loadedUserData != null) { this.UserData = loadedUserData; Debug.WriteLine("Userdata loaded from JSON"); } } catch (Exception ex) { Debug.WriteLine($"Could not load JSON file {ex.Message}"); } }
咨询以下问题:
- 遵循MVVM模式的前提下,如何正确实现JSON文件反序列化?
- 针对该场景,是否有比JSON更优的方案?
- 应用中加载数据的最佳时机是什么?
1. 正确实现JSON反序列化的解决方案
你的问题核心有两点:一是System.Text.Json默认不支持ObservableCollection的反序列化,二是替换单例UserData实例后,其他ViewModel持有的旧实例引用无法同步更新。
修复步骤:
(1)添加ObservableCollection的JSON转换器
System.Text.Json无法直接处理ObservableCollection,需要自定义转换器:
// 全局序列化配置 private static readonly JsonSerializerOptions _jsonOptions = new() { Converters = { new ObservableCollectionConverter() }, WriteIndented = true, IgnoreReadOnlyFields = false }; // 转换器工厂 public class ObservableCollectionConverter : JsonConverterFactory { public override bool CanConvert(Type typeToConvert) { return typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(ObservableCollection<>); } public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options) { Type itemType = type.GetGenericArguments()[0]; return (JsonConverter)Activator.CreateInstance( typeof(ObservableCollectionConverter<>).MakeGenericType(itemType), BindingFlags.Instance | BindingFlags.Public, null, new object[] { options }, null)!; } } // 泛型转换器实现 public class ObservableCollectionConverter<T> : JsonConverter<ObservableCollection<T>> { private readonly JsonConverter<T> _itemConverter; public ObservableCollectionConverter(JsonSerializerOptions options) { _itemConverter = (JsonConverter<T>)options.GetConverter(typeof(T)); } public override ObservableCollection<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType != JsonTokenType.StartArray) throw new JsonException("JSON格式错误:预期数组开头"); var collection = new ObservableCollection<T>(); while (reader.Read()) { if (reader.TokenType == JsonTokenType.EndArray) break; T item = _itemConverter.Read(ref reader, typeof(T), options); collection.Add(item); } return collection; } public override void Write(Utf8JsonWriter writer, ObservableCollection<T> value, JsonSerializerOptions options) { writer.WriteStartArray(); foreach (T item in value) { _itemConverter.Write(writer, item, options); } writer.WriteEndArray(); } }
更新序列化/反序列化方法,使用上述配置:
public static UserData GetData(string jsonFilePath) { try { if (File.Exists(jsonFilePath)) { string data = File.ReadAllText(jsonFilePath); return JsonSerializer.Deserialize<UserData>(data, _jsonOptions); } return null; } catch (Exception ex) { Debug.WriteLine($"加载失败:{ex.Message}"); return null; } } public static void SaveData(string jsonFilePath, UserData userData) { try { string data = JsonSerializer.Serialize(userData, _jsonOptions); File.WriteAllText(jsonFilePath, data); } catch (Exception ex) { Debug.WriteLine($"保存失败:{ex.Message}"); } }
(2)更新现有单例实例的属性,而非替换实例
因为UserData是单例,其他ViewModel已注入初始实例,直接替换this.UserData会导致其他ViewModel引用不同步。正确做法是加载数据后逐个更新属性:
public void LoadUserData(string fileName) { try { string jsonFilePath = Path.Combine(FileSystem.AppDataDirectory, fileName); UserData loadedUserData = JsonService.GetData(jsonFilePath); if (loadedUserData != null) { // 更新集合类型属性 UserData.TakeOffs.Clear(); foreach (var item in loadedUserData.TakeOffs) UserData.TakeOffs.Add(item); UserData.Landings.Clear(); foreach (var item in loadedUserData.Landings) UserData.Landings.Add(item); UserData.RecencyData.Clear(); foreach (var item in loadedUserData.RecencyData) UserData.RecencyData.Add(item); // 更新值类型属性 UserData.LdgExpiration = loadedUserData.LdgExpiration; UserData.TakeOffExpiration = loadedUserData.TakeOffExpiration; UserData.IsRecent = loadedUserData.IsRecent; UserData.RecencyText = loadedUserData.RecencyText; Debug.WriteLine("用户数据已加载"); } } catch (Exception ex) { Debug.WriteLine($"加载JSON文件失败:{ex.Message}"); } }
(3)确保自定义类可序列化
如果RecencyItem是自定义类,需要添加无参构造函数,并标记可序列化的属性:
public partial class RecencyItem : ObservableObject { public RecencyItem() {} // 必须有公共无参构造函数 [ObservableProperty] [JsonInclude] // 允许序列化ObservableProperty生成的私有字段对应的属性 private DateTime date; // 其他属性同理添加[JsonInclude] }
2. 比JSON更优的方案推荐
针对你的场景,可根据需求选择:
- SQLite.NET:适合需要查询、过滤数据的场景(比如按日期筛选起降记录),数据操作更灵活,性能优于JSON。可使用
sqlite-net-pcl库,封装成数据仓库类适配MVVM模式。 - Preferences:如果数据都是键值对类型(单个字符串、整数、DateTime),使用.NET MAUI自带的
Preferences类代码更简洁,但不支持集合类型。 - 二进制序列化:序列化后文件体积更小,但可读性差,版本兼容性不如JSON,适合对存储体积敏感的场景。
总结:如果仅需简单存储读取整个UserData对象,JSON足够用;若后续需要复杂数据操作,优先选择SQLite。
3. 应用加载数据的最佳时机
(1)应用启动时(App.xaml.cs的OnStart方法)
在导航到主页面之前加载数据,用户进入主页面即可看到已保存内容:
public partial class App : Application { private readonly UserData _userData; private readonly JsonService _jsonService; public App(UserData userData, JsonService jsonService) { InitializeComponent(); _userData = userData; _jsonService = jsonService; } protected override void OnStart() { base.OnStart(); string filePath = Path.Combine(FileSystem.AppDataDirectory, "userdata.json"); var loadedData = _jsonService.GetData(filePath); if (loadedData != null) { // 更新现有UserData实例属性,逻辑同之前的LoadUserData _userData.TakeOffs.Clear(); foreach (var item in loadedData.TakeOffs) _userData.TakeOffs.Add(item); // ...其他属性更新 } MainPage = new AppShell(); } }
(2)主ViewModel异步加载
如果数据量较大,同步加载会导致主页面启动延迟,建议异步加载并显示加载状态:
// 在MainViewModel中添加ObservableProperty用于控制加载状态 [ObservableProperty] private bool isLoading; public async Task LoadUserDataAsync(string fileName) { try { IsLoading = true; string jsonFilePath = Path.Combine(FileSystem.AppDataDirectory, fileName); string data = await File.ReadAllTextAsync(jsonFilePath); UserData loadedUserData = JsonSerializer.Deserialize<UserData>(data, _jsonOptions); if (loadedUserData != null) { // 更新属性逻辑 UserData.TakeOffs.Clear(); foreach (var item in loadedUserData.TakeOffs) UserData.TakeOffs.Add(item); // ...其他属性 } } catch (Exception ex) { Debug.WriteLine($"加载失败:{ex.Message}"); } finally { IsLoading = false; } } // 构造函数中调用异步方法 public MainViewModel(UserData userData) { this.UserData = userData; _ = LoadUserDataAsync(JSON_FILE_NAME); }
最佳实践:数据量小则在App启动时同步加载;数据量大则在主ViewModel异步加载,配合页面加载动画避免UI卡顿。
内容的提问来源于stack exchange,提问作者donot

