Core - Fix data model paths around saving, removed children and stored types

- The constructors saved the path before setting it up, while it was
  still invalid, so a new path stored nothing and didn't follow its data
  model leaving and returning to the store until it was saved.
- Removing the dynamic child a path points to left the path valid and
  reading a default value, it now becomes invalid like when the child
  was never there.
- The stored type of a path is now also looked up in all loaded
  assemblies, Type.GetType only found it in Artemis.Core and the .NET
  libraries, not in plugins or SkiaSharp.
This commit is contained in:
RobertBeekman committed 2026-10-20 09:27:53 +02:00
1 parent bbaf21c25f
commit edc7ec7705
3 files changed
+57 -28

No files matched your search

@@ -340,7 +340,7 @@ public class DataModelPathTests : IDisposable
} }
[Fact] [Fact]
public void RemovingTheDynamicChild_LeavesThePathValid() public void RemovingTheDynamicChild_MakesThePathInvalid()
{ {
DataModel.AddDynamicChild("Extra", 5); DataModel.AddDynamicChild("Extra", 5);
using DataModelPath path = new(DataModel, "Extra"); using DataModelPath path = new(DataModel, "Extra");
@@ -349,25 +349,34 @@ public class DataModelPathTests : IDisposable
DataModel.RemoveDynamicChildByKey("Extra"); DataModel.RemoveDynamicChildByKey("Extra");
// Pinned as-is: the segment only invalidates the path when the value of the removed child is the data model the path.IsValid.ShouldBeFalse();
// segment looks in, rather than when the child it points to is removed. So the path stays valid and reads the path.GetValue().ShouldBeNull();
// default value instead of becoming invalid. invalidated.ShouldBe(1);
path.IsValid.ShouldBeTrue();
path.GetValue().ShouldBe(0);
invalidated.ShouldBe(0);
} }
[Fact] [Fact]
public void RemovingADynamicDataModel_LeavesThePathThroughItValid() public void RemovingTheDynamicChild_AndAddingItAgain_MakesThePathValidAgain()
{
DataModel.AddDynamicChild("Extra", 5);
using DataModelPath path = new(DataModel, "Extra");
DataModel.RemoveDynamicChildByKey("Extra");
DataModel.AddDynamicChild("Extra", 6);
path.IsValid.ShouldBeTrue();
path.GetValue().ShouldBe(6);
}
[Fact]
public void RemovingADynamicDataModel_MakesThePathThroughItInvalid()
{ {
DataModel.AddDynamicChild("Extra", new TestChildDataModel {Integer = 4}, "Extra data"); DataModel.AddDynamicChild("Extra", new TestChildDataModel {Integer = 4}, "Extra data");
using DataModelPath path = new(DataModel, "Extra.Integer"); using DataModelPath path = new(DataModel, "Extra.Integer");
DataModel.RemoveDynamicChildByKey("Extra"); DataModel.RemoveDynamicChildByKey("Extra");
// Pinned as-is, see RemovingTheDynamicChild_LeavesThePathValid path.IsValid.ShouldBeFalse();
path.IsValid.ShouldBeTrue(); path.GetValue().ShouldBeNull();
path.GetValue().ShouldBe(0);
} }
[Fact] [Fact]
@@ -450,20 +459,17 @@ public class DataModelPathTests : IDisposable
} }
[Fact] [Fact]
public void NewPath_DoesNotFollowTheStoreUntilItIsSaved() public void NewPath_FollowsTheStoreWithoutBeingSaved()
{ {
using DataModelPath path = new(DataModel, "Integer"); using DataModelPath path = new(DataModel, "Integer");
// Pinned as-is: the constructors call Save before the path is set up, while it is still invalid, so nothing is path.Entity.DataModelId.ShouldBe(_module.Id);
// saved and the entity has no data model ID. The path only reacts to its data model leaving and returning to the path.Entity.Path.ShouldBe("Integer");
// store once Save is called. Looks like Save should be called after Initialize.
path.Entity.DataModelId.ShouldBeNull();
path.Entity.Path.ShouldBe("");
RemoveFromStore(); RemoveFromStore();
path.IsValid.ShouldBeTrue(); path.IsValid.ShouldBeFalse();
path.Target.ShouldBeSameAs(DataModel); path.Target.ShouldBeNull();
} }
[Fact] [Fact]
@@ -540,7 +546,7 @@ public class DataModelPathTests : IDisposable
} }
[Fact] [Fact]
public void InvalidPath_CannotFallBackToATypeOutsideTheCoreLibraries() public void InvalidPath_FallsBackToATypeOutsideTheCoreLibraries()
{ {
using DataModelPath valid = new(DataModel, "Enum"); using DataModelPath valid = new(DataModel, "Enum");
valid.Save(); valid.Save();
@@ -548,9 +554,15 @@ public class DataModelPathTests : IDisposable
using DataModelPath path = new(new DataModelPathEntity {Path = "Enum", DataModelId = "Missing-12345678", Type = valid.Entity.Type}); using DataModelPath path = new(new DataModelPathEntity {Path = "Enum", DataModelId = "Missing-12345678", Type = valid.Entity.Type});
// Pinned as-is: the type is saved by its full name without the assembly, which Type.GetType can only find in path.GetPropertyType().ShouldBe(typeof(TestEnum));
// Artemis.Core and the .NET libraries, so types from plugins (and SkiaSharp, like SKColor) can't be found again. }
path.GetPropertyType().ShouldBeNull();
[Fact]
public void InvalidPath_FallsBackToASkiaSharpType()
{
using DataModelPath path = new(new DataModelPathEntity {Path = "Color", DataModelId = "Missing-12345678", Type = typeof(SKColor).FullName});
path.GetPropertyType().ShouldBe(typeof(SKColor));
} }
[Fact] [Fact]
@@ -29,8 +29,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = []; _segments = [];
Save();
Initialize(); Initialize();
Save();
SubscribeToDataModelStore(); SubscribeToDataModelStore();
} }
@@ -47,8 +47,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = []; _segments = [];
Save();
Initialize(); Initialize();
Save();
SubscribeToDataModelStore(); SubscribeToDataModelStore();
} }
@@ -67,8 +67,8 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
_segments = []; _segments = [];
Save();
Initialize(); Initialize();
Save();
SubscribeToDataModelStore(); SubscribeToDataModelStore();
} }
@@ -165,11 +165,20 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
// Fall back to stored type // Fall back to stored type
if (!string.IsNullOrWhiteSpace(Entity.Type)) if (!string.IsNullOrWhiteSpace(Entity.Type))
return Type.GetType(Entity.Type); return FindType(Entity.Type);
return null; return null;
} }
private static Type? FindType(string fullName)
{
// The type is stored without its assembly, Type.GetType only finds it in Artemis.Core and the .NET libraries. Types
// of plugins and other libraries (like SKColor) are found by looking through the loaded assemblies
return Type.GetType(fullName) ?? AppDomain.CurrentDomain.GetAssemblies()
.Select(assembly => assembly.GetType(fullName))
.FirstOrDefault(type => type != null);
}
/// <summary> /// <summary>
/// Gets the property description of the property this path points to /// Gets the property description of the property this path points to
/// </summary> /// </summary>
@@ -286,8 +286,16 @@ public class DataModelPathSegment : IDisposable
private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e) private void DynamicChildOnDynamicChildRemoved(object? sender, DynamicDataModelChildEventArgs e)
{ {
if (e.DynamicChild.BaseValue == _dynamicDataModel) // Like when a child is added, initialize the path again so it no longer points to the removed child
if (e.Key == Identifier)
{
DataModelPath.Invalidate(); DataModelPath.Invalidate();
DataModelPath.Initialize();
}
else if (e.DynamicChild.BaseValue == _dynamicDataModel)
{
DataModelPath.Invalidate();
}
} }
/// <inheritdoc /> /// <inheritdoc />