Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 47 additions & 15 deletions Datra.Editor/Models/FieldCreationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,27 @@ public class FieldCreationContext
/// <summary>루트 데이터 객체</summary>
public object? RootDataObject { get; set; }

/// <summary>
/// 현재 필드가 파생된 원본 멤버.
/// 배열/리스트/dictionary 요소처럼 Property/Member가 없는 파생 필드에서도
/// attribute 기반 핸들러가 원래 선언부의 메타데이터를 볼 수 있게 한다.
/// </summary>
public MemberInfo? SourceMember { get; set; }

/// <summary>루트 편집 객체 기준의 사람이 읽을 수 있는 필드 경로.</summary>
public string? FieldPath { get; set; }

/// <summary>컬렉션 요소 키 (dictionary 요소인 경우)</summary>
public object? CollectionElementKey { get; set; }

/// <summary>중첩 멤버인지 여부</summary>
public bool IsNestedMember => Member != null && Property == null;

/// <summary>배열 요소인지 여부</summary>
public bool IsArrayElement => Property == null && Member == null && CollectionElementIndex.HasValue;
/// <summary>배열/리스트 요소인지 여부</summary>
public bool IsArrayElement => Property == null && Member == null && CollectionElementIndex.HasValue && CollectionElementKey == null;

/// <summary>배열/리스트/dictionary 요소인지 여부</summary>
public bool IsCollectionElement => CollectionElementIndex.HasValue || CollectionElementKey != null;

/// <summary>
/// 프로퍼티 기반 필드 생성용 생성자
Expand All @@ -76,6 +92,8 @@ public FieldCreationContext(
OnValueChanged = onValueChanged;
LocaleService = localeService;
IsReadOnly = isReadOnly || !property.CanWrite;
SourceMember = property;
FieldPath = property.Name;
}

/// <summary>
Expand All @@ -100,6 +118,8 @@ public FieldCreationContext(
OnValueChanged = onValueChanged;
LocaleService = localeService;
IsReadOnly = isReadOnly;
SourceMember = member;
FieldPath = member.Name;
}

/// <summary>
Expand All @@ -112,7 +132,9 @@ public FieldCreationContext(
FieldLayoutMode layoutMode,
Action<object?>? onValueChanged,
ILocaleEditorService? localeService = null,
bool isReadOnly = false)
bool isReadOnly = false,
MemberInfo? sourceMember = null,
string? fieldPath = null)
{
FieldType = elementType;
Target = value;
Expand All @@ -122,6 +144,8 @@ public FieldCreationContext(
OnValueChanged = onValueChanged;
LocaleService = localeService;
IsReadOnly = isReadOnly;
SourceMember = sourceMember;
FieldPath = fieldPath;
}

/// <summary>
Expand All @@ -131,29 +155,29 @@ public FieldCreationContext WithValue(object? newValue)
{
if (Property != null)
{
return new FieldCreationContext(Property, Target, newValue, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(Property, Target, newValue, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElementIndex = CollectionElementIndex,
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
else if (Member != null)
{
return new FieldCreationContext(Member, FieldType, ParentValue, newValue, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(Member, FieldType, ParentValue, newValue, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElementIndex = CollectionElementIndex,
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
else
{
return new FieldCreationContext(FieldType, newValue, CollectionElementIndex ?? 0, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(FieldType, newValue, CollectionElementIndex ?? 0, LayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
}

Expand All @@ -164,30 +188,38 @@ public FieldCreationContext WithLayoutMode(FieldLayoutMode newLayoutMode)
{
if (Property != null)
{
return new FieldCreationContext(Property, Target, Value, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(Property, Target, Value, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElementIndex = CollectionElementIndex,
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
else if (Member != null)
{
return new FieldCreationContext(Member, FieldType, ParentValue, Value, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(Member, FieldType, ParentValue, Value, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElementIndex = CollectionElementIndex,
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
else
{
return new FieldCreationContext(FieldType, Value, CollectionElementIndex ?? 0, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
return CopyMetadataTo(new FieldCreationContext(FieldType, Value, CollectionElementIndex ?? 0, newLayoutMode, OnValueChanged, LocaleService, IsReadOnly)
{
CollectionElement = CollectionElement,
RootDataObject = RootDataObject
};
});
}
}

private FieldCreationContext CopyMetadataTo(FieldCreationContext copy)
{
copy.SourceMember = SourceMember;
copy.FieldPath = FieldPath;
copy.CollectionElementKey = CollectionElementKey;
return copy;
}
}
}
2 changes: 1 addition & 1 deletion Datra.Editor/Schema/TypeClassifier.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ private static bool IsGenericListLike(Type type)
return GetElementType(type) != null;
}

private static bool TryGetDictionaryArgs(Type type, out Type? keyType, out Type? valueType)
public static bool TryGetDictionaryArgs(Type type, out Type? keyType, out Type? valueType)
{
keyType = null;
valueType = null;
Expand Down
3 changes: 2 additions & 1 deletion Datra.Tests/Datra.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
<ProjectReference Include="..\Datra.SampleData2\Datra.SampleData2.csproj" />
<ProjectReference Include="..\Datra\Datra.csproj" />
<ProjectReference Include="..\Datra.Editor\Datra.Editor.csproj" />
<ProjectReference Include="..\Datra.WebEditor\Datra.WebEditor.csproj" />
</ItemGroup>

<ItemGroup>
Expand All @@ -49,4 +50,4 @@
<AdditionalFiles Include="..\Datra.SampleData\Resources\Localizations\LocalizationKeys.csv" Link="LocalizationKeys.csv" />
</ItemGroup>

</Project>
</Project>
145 changes: 145 additions & 0 deletions Datra.Tests/DatraWebEditorFieldHandlerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Datra.Editor.Interfaces;
using Datra.Editor.Models;
using Datra.WebEditor.Abstractions;
using Datra.WebEditor.Handlers;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Xunit;

namespace Datra.Tests
{
public class DatraWebEditorFieldHandlerTests
{
[Fact]
public void RegisterDefaultHandlers_IncludesDictionaryHandler()
{
var registry = new BlazorFieldTypeRegistry();

registry.RegisterDefaultHandlers();

var handler = registry.FindBlazorHandler(typeof(Dictionary<string, string>));
Assert.IsType<DictionaryFieldHandler>(handler);
}

[Fact]
public void ListFieldHandler_PropagatesSourceMemberToElementHandler()
{
var registry = CreateRegistryWithCapturingHandler(out var capture);
var data = new TestEditorData { Effects = new List<string> { "fx.spark" } };
var prop = typeof(TestEditorData).GetProperty(nameof(TestEditorData.Effects))!;
var context = new FieldCreationContext(prop, data, data.Effects, FieldLayoutMode.Form, _ => { });

var handler = registry.FindBlazorHandler(prop.PropertyType, prop);
Assert.IsType<ListFieldHandler>(handler);

Render(handler!.CreateField(context));

var elementContext = Assert.Single(capture.Contexts);
Assert.Same(prop, elementContext.SourceMember);
Assert.Equal(0, elementContext.CollectionElementIndex);
Assert.Equal("fx.spark", elementContext.CollectionElement);
Assert.Equal("Effects[0]", elementContext.FieldPath);
}

[Fact]
public void ArrayFieldHandler_PropagatesSourceMemberToElementHandler()
{
var registry = CreateRegistryWithCapturingHandler(out var capture);
var data = new TestEditorData { Sprites = new[] { "sprite.hero" } };
var prop = typeof(TestEditorData).GetProperty(nameof(TestEditorData.Sprites))!;
var context = new FieldCreationContext(prop, data, data.Sprites, FieldLayoutMode.Form, _ => { });

var handler = registry.FindBlazorHandler(prop.PropertyType, prop);
Assert.IsType<ArrayFieldHandler>(handler);

Render(handler!.CreateField(context));

var elementContext = Assert.Single(capture.Contexts);
Assert.Same(prop, elementContext.SourceMember);
Assert.Equal(0, elementContext.CollectionElementIndex);
Assert.Equal("sprite.hero", elementContext.CollectionElement);
Assert.Equal("Sprites[0]", elementContext.FieldPath);
}

[Fact]
public void DictionaryFieldHandler_PropagatesSourceMemberToValueHandlerOnly()
{
var registry = CreateRegistryWithCapturingHandler(out var capture);
var data = new TestEditorData
{
MaterialBySlot = new Dictionary<string, string>
{
["baseColor"] = "mat.wall"
}
};
var prop = typeof(TestEditorData).GetProperty(nameof(TestEditorData.MaterialBySlot))!;
var context = new FieldCreationContext(prop, data, data.MaterialBySlot, FieldLayoutMode.Form, _ => { });

var handler = registry.FindBlazorHandler(prop.PropertyType, prop);
Assert.IsType<DictionaryFieldHandler>(handler);

Render(handler!.CreateField(context));

var valueContext = Assert.Single(capture.Contexts);
Assert.Same(prop, valueContext.SourceMember);
Assert.Equal(0, valueContext.CollectionElementIndex);
Assert.Equal("baseColor", valueContext.CollectionElementKey);
Assert.Equal("mat.wall", valueContext.CollectionElement);
Assert.Equal("MaterialBySlot[baseColor]", valueContext.FieldPath);
}

private static BlazorFieldTypeRegistry CreateRegistryWithCapturingHandler(out CapturingAssetStringHandler capture)
{
var registry = new BlazorFieldTypeRegistry();
capture = new CapturingAssetStringHandler();

registry.RegisterHandler(capture);
registry.RegisterHandler(new DictionaryFieldHandler(registry));
registry.RegisterHandler(new ArrayFieldHandler(registry));
registry.RegisterHandler(new ListFieldHandler(registry));
registry.RegisterHandler(new StringFieldHandler());
return registry;
}

private static void Render(RenderFragment fragment)
{
var builder = new RenderTreeBuilder();
fragment(builder);
}

private sealed class CapturingAssetStringHandler : IBlazorFieldHandler
{
public List<FieldCreationContext> Contexts { get; } = new();
public int Priority => 100;

public bool CanHandle(Type type, MemberInfo? member = null)
=> type == typeof(string) && member?.GetCustomAttribute<TestAssetAttribute>() is not null;

public RenderFragment CreateField(FieldCreationContext context) => builder =>
{
Contexts.Add(context);
builder.AddContent(0, "asset");
};
}

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
private sealed class TestAssetAttribute : Attribute
{
}

private sealed class TestEditorData
{
[TestAsset]
public List<string> Effects { get; set; } = new();

[TestAsset]
public string[] Sprites { get; set; } = Array.Empty<string>();

[TestAsset]
public Dictionary<string, string> MaterialBySlot { get; set; } = new();
}
}
}
Loading
Loading