You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET MAUI本地保存加载特定用户数据的实现方案咨询

.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}");
    }
}

咨询以下问题:

  1. 遵循MVVM模式的前提下,如何正确实现JSON文件反序列化?
  2. 针对该场景,是否有比JSON更优的方案?
  3. 应用中加载数据的最佳时机是什么?

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 15:15:55