用于 WPF 绑定的对象的序列化/反序列化

Serialization/deserialization of an object that is used for WPF binding

提问人:Justinas Rubinovas 提问时间:10/31/2023 更新时间:10/31/2023 访问量:57

问:

在我的应用程序中,我有一个数据结构,用于保存用户输入和其他信息,其中大部分数据用于 MVVM WPF 绑定。此结构具有嵌套类、自定义对象的实例、列表等。您不必通读所有内容,但这只是其中的摘录:

public class StudyData
{
    public StudyInputData InputData { get; init; } = new();

    public class StudyInputData : StudyDataCategory
    {
        public VesselData Vessel { get; init; } = new();
        public EnvironmentData Environment { get; init; } = new();
        public VesEnvInteractionData Interaction { get; init; } = new();

        public class VesselData : StudyDataCategory
        {
            public BuoyancyScope buoyancyScope { get; init; } = new();
            public LoadingCondition loadingCondition { get; init; } = new();
            public Resistance resistance { get; init; } = new();

            public class BuoyancyScope : StudyDataCategory
            {
                public StudyInputDataPropertyICadObjectInfos BuoyancyScopeCadObjectsIncluded { get; init; } = new(StudyInputDataPropertyNames.BuoyancyScopeCadObjectsIncluded);
                public StudyInputDataPropertyICadObjectInfos BuoyancyScopeCadObjectsExcluded { get; init; } = new(StudyInputDataPropertyNames.BuoyancyScopeCadObjectsExcluded);
            }
            public class Resistance : StudyDataCategory
            {
                public StudyInputDataPropertyString resistanceAlgorithmName { get; init; } = new("Resistance algorithm", defaultValue: Utils.GetEnumDescription(ResistanceAlgorithmNames.kaper));
            }
        }
        public class EnvironmentData : StudyDataCategory
        {
            public StudyInputDataPropertyString UpperMediumMaterial { get; init; } = new(StudyInputDataPropertyNames.UpperMediumMaterial) { Value = "Air" }; 
            public StudyInputDataPropertyString LowerMediumMaterial { get; init; } = new(StudyInputDataPropertyNames.LowerMediumMaterial) { Value = "Fresh water" };
            public StudyInputDataPropertyMultiNumeric GravityConstant { get; init; } = new(StudyInputDataPropertyNames.LowerMediumMaterial) { InputString = "9.81" };
        }
        public class VesEnvInteractionData : StudyDataCategory
        {
            public Immersion immersion { get; init; } = new();
            public ForcesMoments forcesMoments { get; init; } = new();

            public class Immersion : StudyDataCategory
            {
                public StudyInputDataPropertyBool CalculateImmersion { get; init; } = new(StudyInputDataPropertyNames.CalculateImmersion, defaultValue = false);
                public StudyInputDataPropertyMultiNumeric Draft { get; init; } = new(StudyInputDataPropertyNames.Draft);
                public StudyInputDataPropertyMultiNumeric Heel { get; init; } = new(StudyInputDataPropertyNames.Heel, defaultInputString: "0");
                public StudyInputDataPropertyMultiNumeric Trim { get; init; } = new(StudyInputDataPropertyNames.Trim, defaultInputString: "0");
            }

            public class ForcesMoments : StudyDataCategory
            {
                public ObservableCollection<IStudyInputDataPropertyForce> Forces { get; init; } = new()
                {
                    new StudyInputDataPropertyWeightForce("Weight force"),
                    new StudyInputDataPropertyLiftingForce("Lifting"),
                    new StudyInputDataPropertyDrivingForce("Driving force"),
                    new StudyInputDataPropertyWindageForce("Windage")
                };
            }           
        }
    }
}

请注意,这些对象中的最终基元类型变量要么实现 ,要么属于使绑定工作的类型。INotifyPropertyChangedObservableCollection

我想通过在用户进行一些修改后序列化为 JSON 文件来实现文件另存为 / 文件打开功能,然后将其反序列化回来,完全按原样进行反序列化,而不会破坏 WPF 绑定。StudyData

目前,我正在使用 JSON 反序列化来完全替换 的实例,它提供了 100% 保证反序列化的对象与序列化的对象完全相同,但这会破坏 WPF 绑定,因为 仍然引用 的旧实例。为了解决这个问题,我必须在反序列化后手动更新每个 UI 元素,或者使用奇怪的恶作剧绑定代码隐藏,例如:StudyDataDataContextStudyDataDataContextStudyData

string path = "study.Data.InputsData.Interaction.Immersion.CalculateImmersion.Value"
Binding binding = new Binding() { Path = new PropertyPath(path) };
item.SetBinding(ImmersionCalculationEnabledCheckBox.IsChecked, binding);

此解决方法有其局限性,例如无法绑定到用户可能已添加到该列表的列表元素,因为它不是不同的属性等。我也知道这不是 MVVM,总体上不是一个好的编码实践。

有人告诉我,我可以使用 DTO 进行序列化,以将 UI 绑定问题与序列化问题分开。我对 DTO 进行了一些研究,因为它对我来说是一个新概念,据我了解,我必须将每个属性从实例显式映射到 DTO。这对我来说似乎真的无法维护,因为有几千个变量,每次我添加/更改此数据结构时,我都必须手动更新 DTO。StudyDataStudyData

我正在考虑的另一个想法是将 JSON 反序列化为 的单独实例,然后使用 Reflection 传输端点基元类型变量的值,这与 DTO 的想法基本相同,而无需手动映射所有内容。但是,我不知道如何以一种与结构无关的自动化方式做到这一点,这在开发过程中可能仍然会发生很大变化。StudyDataStudyInputData

我发现的另一个想法是使用 ,这不应该破坏绑定,但显然它不适用于 AND 正如我需要的那样,因为它会将反序列化的项目添加到列表中而不是替换它们,从而导致重复。JsonConvert.PopulateObjectListObservableCollection

TLDR:我觉得我错过了一些用于这种情况的简单佳能解决方案。我的要求很简单:以全自动且免维护的方式序列化和反序列化数据结构,这种方式与内部结构和复杂性无关,并且在反序列化后生成相同的对象,而不会破坏 WPF 绑定。StudyDataStudyData

谁能建议我应该在这里做什么?

C# JSON WPF 序列化

评论

0赞 JonasH 10/31/2023
当您更改 时,是否引发了 PropertyChanged-event?StudyData
1赞 JonasH 10/31/2023
此外,您通常无法实现“全自动免维护反序列化”的愿望。总会有一些缓存的值、事件或其他详细信息未被序列化捕获,需要手动处理。您需要考虑如何完成序列化,以及如何处理到模型的转换,而 DTO 的优势在于明确了这一点。
0赞 Clemens 10/31/2023
(父)视图模型类中应有一个 StudyData 类型的属性。只需将该视图模型的实例分配给视图的 DataContext 一次。StudyData 属性将触发更改通知。
0赞 Justinas Rubinovas 10/31/2023
@JonasH,是的,我确实为 StudyData 筹集了 PropertyChanged。当我试图简化它时,我从这个例子中省略了它。你说得对,DTO 是明确的,但就我而言,它太显式了,每次我需要更改此数据结构中的某些内容时,它都会使工作量加倍。
1赞 Andy 11/1/2023
I would have a viewmodel and model for each class and use mapperly to copy between them. JSON is a bit more sensitive whe it comes to serialisation and deserialisation. I suggest you consider messagepack. That relies on an attribute for tag number identifying each field rather than name. This avoids some complications you can get with changing namespaces. It's also a more efficient serialisation format.

答: 暂无答案