.NET Maui中LiteDB存储SkiaSharp数据加载遇InvalidCastException求助
问题诊断
你遇到的System.InvalidCastException根源是LiteDB默认无法正确序列化/反序列化SkiaSharp的非POCO类型:
SKPoint是值类型(struct),如果数据库中对应字段因序列化问题存为null,反序列化时无法将null转换为值类型,直接抛出异常。SKPaint是SkiaSharp的复杂对象,包含大量内部状态,LiteDB的默认序列化器无法完整处理它的所有属性,导致部分数据丢失或存储为null。
解决方案
下面提供两种可行的解决思路,推荐第二种(中间POCO类),更易维护和控制存储内容。
方案1:自定义LiteDB Bson映射
为SKPoint和SKPaint实现自定义序列化/反序列化逻辑,告诉LiteDB如何处理这些类型:
1. 注册自定义序列化器
在应用启动时(比如MauiProgram.cs的CreateMauiApp方法中)添加以下代码:
using LiteDB; using SkiaSharp; // 为SKPoint注册自定义序列化器 BsonMapper.Global.RegisterType( (SKPoint point) => new BsonDocument { ["X"] = point.X, ["Y"] = point.Y }, (bson) => new SKPoint(bson["X"].AsSingle, bson["Y"].AsSingle) ); // 为SKPaint注册自定义序列化器(只存储绘图必需的属性,按需添加) BsonMapper.Global.RegisterType( (SKPaint paint) => new BsonDocument { ["Color"] = paint.Color.ToArgb(), ["StrokeWidth"] = paint.StrokeWidth, ["Style"] = paint.Style.ToString(), ["IsAntialias"] = paint.IsAntialias }, (bson) => new SKPaint { Color = new SKColor((int)bson["Color"].AsInt32), StrokeWidth = bson["StrokeWidth"].AsSingle, Style = Enum.Parse<SKPaintStyle>(bson["Style"].AsString), IsAntialias = bson["IsAntialias"].AsBoolean } );
2. 重新测试数据读写
注册完成后,LiteDB会使用自定义逻辑处理SKPoint和SKPaint,避免值类型null转换问题,同时只存储必要的绘图属性。
方案2:使用中间POCO类(推荐)
创建与Stroke对应的普通CLR对象(POCO),将SkiaSharp对象转换为POCO后存储,读取时再转换回SkiaSharp对象:
1. 创建可序列化的POCO类
// 用于存储SKPoint的POCO public class StrokePoint { public float X { get; set; } public float Y { get; set; } // 转换方法 public static StrokePoint FromSKPoint(SKPoint point) => new StrokePoint { X = point.X, Y = point.Y }; public SKPoint ToSKPoint() => new SKPoint(X, Y); } // 用于存储SKPaint核心属性的POCO public class StrokePaint { public int ColorArgb { get; set; } public float StrokeWidth { get; set; } public string Style { get; set; } public bool IsAntialias { get; set; } // 转换方法 public static StrokePaint FromSKPaint(SKPaint paint) => new StrokePaint { ColorArgb = paint.Color.ToArgb(), StrokeWidth = paint.StrokeWidth, Style = paint.Style.ToString(), IsAntialias = paint.IsAntialias }; public SKPaint ToSKPaint() => new SKPaint { Color = new SKColor(ColorArgb), StrokeWidth = StrokeWidth, Style = Enum.Parse<SKPaintStyle>(Style), IsAntialias = IsAntialias }; } // 可序列化的Stroke POCO public class StrokeDto { public StrokePaint Paint { get; set; } public List<StrokePoint> Points { get; set; } = new List<StrokePoint>(); // 与原Stroke类的转换 public static StrokeDto FromStroke(Stroke stroke) => new StrokeDto { Paint = StrokePaint.FromSKPaint(stroke.Paint), Points = stroke.Points.Select(StrokePoint.FromSKPoint).ToList() }; public Stroke ToStroke() => new Stroke { Paint = Paint.ToSKPaint(), Points = Points.Select(p => p.ToSKPoint()).ToList() }; } // 修改NSketch类,存储Dto而非原Stroke public class NSketch: NotebookItem { public List<StrokeDto> StrokeDtos { get; set; } [BsonIgnore] // 提供原Strokes的访问器,自动转换Dto public List<Stroke> Strokes { get => StrokeDtos?.Select(dto => dto.ToStroke()).ToList() ?? new List<Stroke>(); set => StrokeDtos = value?.Select(StrokeDto.FromStroke).ToList() ?? new List<StrokeDto>(); } [BsonIgnore] public override string Description => "Skizze"; public NSketch() { StrokeDtos = new List<StrokeDto>(); } }
2. 数据读写逻辑无需修改
此时NSketch类的StrokeDtos是完全可序列化的POCO,LiteDB可以正常存储和读取,而Strokes属性通过BsonIgnore标记,不会被LiteDB处理,仅作为业务代码的访问入口自动转换。
验证
两种方案都可以解决InvalidCastException问题,方案2的优势在于:
- 完全控制存储的数据内容,避免存储不必要的SkiaSharp内部状态
- 代码更清晰,后续维护和扩展更方便
内容的提问来源于stack exchange,提问作者Expert1982
相关产品推荐
相关产品推荐

