重构反射生成属性界面
This commit is contained in:
-19
@@ -1,19 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcd1c64483d2457ab9143f086dff511d, type: 3}
|
||||
m_Name: XuiavrgButton-Default
|
||||
m_EditorClassIdentifier:
|
||||
themeName: Default
|
||||
uiName: Default
|
||||
valueTypeName: System.Single
|
||||
prefab: {fileID: 6894309230704738580, guid: a58d335e7df1a994d9f1c0fd58aff68f, type: 3}
|
||||
supportedTags: 010000001000000002000000
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f4eb311896b80d4b9ced716a740dfad
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcd1c64483d2457ab9143f086dff511d, type: 3}
|
||||
m_Name: XuiavrgDropdown-Default
|
||||
m_EditorClassIdentifier:
|
||||
themeName: Default
|
||||
uiName: Default
|
||||
valueTypeName: System.Enum
|
||||
prefab: {fileID: 1100600009542095382, guid: 4f25b16125eac234e820506fff28a4b0, type: 3}
|
||||
supportedTags: 01000000
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a13481739bcb4d468fced5fde896f0f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcd1c64483d2457ab9143f086dff511d, type: 3}
|
||||
m_Name: XuiavrgFieldInput-Default
|
||||
m_EditorClassIdentifier:
|
||||
themeName: Default
|
||||
uiName: Default
|
||||
valueTypeName: System.String
|
||||
prefab: {fileID: 8186464640817935025, guid: 1715f06fee075eb45b68d43575e9aa88, type: 3}
|
||||
supportedTags: 0100000080000000
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4aa3fe7aa30f1b240a4486c12152cd4d
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcd1c64483d2457ab9143f086dff511d, type: 3}
|
||||
m_Name: XuiavrgFloatSlider-Default
|
||||
m_EditorClassIdentifier:
|
||||
themeName: Default
|
||||
uiName: Default
|
||||
valueTypeName: System.Single
|
||||
prefab: {fileID: 3950712135413418637, guid: f601d441cf135fe479d319c5eb01abd8, type: 3}
|
||||
supportedTags: 010000002000000024000000
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 496a772f348d94c419faf2f5c09b773f
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
%YAML 1.1
|
||||
%TAG !u! tag:unity3d.com,2011:
|
||||
--- !u!114 &11400000
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 0}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: bcd1c64483d2457ab9143f086dff511d, type: 3}
|
||||
m_Name: XuiavrgToggle-Default
|
||||
m_EditorClassIdentifier:
|
||||
themeName: Default
|
||||
uiName: Default
|
||||
valueTypeName: System.Boolean
|
||||
prefab: {fileID: 8328042880981269257, guid: 0dc894baa18f5cb4eb9c82fd3c40d2bd, type: 3}
|
||||
supportedTags: 0100000008000000
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41a9df21e888bf3409ef31a6079aab61
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+11
-3
@@ -1,7 +1,15 @@
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 反射 UI Attribute 标记接口
|
||||
/// 所有标记 Attribute 实现此接口,在元数据提取阶段按序执行 BindMetaData
|
||||
/// </summary>
|
||||
public interface IXuiavrgAttribute
|
||||
{
|
||||
public void BindProperty(XuiavrgInspectorProperty property);
|
||||
/// <summary>
|
||||
/// 将 Attribute 的配置绑定到元数据对象
|
||||
/// </summary>
|
||||
/// <param name="metaData">目标元数据</param>
|
||||
public void BindMetaData(AvrgMemberMetaData metaData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 反射属性标记
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
|
||||
public class XuiavrgAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
#region 字段属性
|
||||
|
||||
public readonly string LabelName;
|
||||
public readonly string UIPropertyName;
|
||||
public readonly string UnitLabelName;
|
||||
public readonly int InspectorOrder;
|
||||
public readonly string GroupName;
|
||||
public readonly string OnTriggerCall;
|
||||
public readonly XuiavrgSize ArgumentSize;
|
||||
|
||||
/// <summary>
|
||||
/// 标记属性
|
||||
/// </summary>
|
||||
/// <param name="labelName">属性显示名称</param>
|
||||
/// <param name="uiPropertyName">属性标记名(对应界面元素名称)</param>
|
||||
/// <param name="unitLabelName">属性显示单位名称</param>
|
||||
/// <param name="mustFillin">是否标记为必填项目</param>
|
||||
/// <param name="inspectorOrder">属性显示顺序</param>
|
||||
/// <param name="groupName">属性组名称(可以是以/&\分割的路径)</param>
|
||||
/// <param name="onTriggerCall">属性附加的动作,调用此内联反射调用的函数</param>
|
||||
/// <param name="size">属性组名称(可以是以/&\分割的路径)</param>
|
||||
public XuiavrgAttribute(string labelName, string uiPropertyName, string unitLabelName,
|
||||
int inspectorOrder = 0, string groupName = null, string onTriggerCall = "", XuiavrgSize size = XuiavrgSize.Normal)
|
||||
{
|
||||
LabelName = labelName;
|
||||
UIPropertyName = uiPropertyName;
|
||||
UnitLabelName = unitLabelName;
|
||||
InspectorOrder = inspectorOrder;
|
||||
GroupName = groupName;
|
||||
OnTriggerCall = onTriggerCall;
|
||||
ArgumentSize = size;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生成器
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.labelName = LabelName;
|
||||
property.uiPropertyName = UIPropertyName;
|
||||
property.unitLabelName = UnitLabelName;
|
||||
property.inspectorOrder = InspectorOrder;
|
||||
property.groupName = GroupName;
|
||||
property.onTriggerCall = OnTriggerCall;
|
||||
property.argumentSize = ArgumentSize;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 属性尺寸
|
||||
/// </summary>
|
||||
public enum XuiavrgSize
|
||||
{
|
||||
None = 0,
|
||||
|
||||
Tiny = 1000 - 2,
|
||||
Small = 1000 - 1,
|
||||
Normal = 1000,
|
||||
Big,
|
||||
Large,
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10e270c86cab4817a0b8ce7877b2ab88
|
||||
timeCreated: 1776505683
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 集合检查
|
||||
/// </summary>
|
||||
public class XuiavrgCollectionCheckAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
public readonly int minCount;
|
||||
public readonly int maxCount;
|
||||
public readonly bool notEmpty;
|
||||
|
||||
public XuiavrgCollectionCheckAttribute(int minCount, int maxCount, bool notEmpty)
|
||||
{
|
||||
this.minCount = minCount;
|
||||
this.maxCount = maxCount;
|
||||
this.notEmpty = notEmpty;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.collectionMinCount = minCount;
|
||||
property.collectionMaxCount = maxCount;
|
||||
property.collectionNotEmpty = notEmpty;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ec4224cb3844c439dd5fdb6b31c9730
|
||||
timeCreated: 1777363387
|
||||
+4
-3
@@ -38,10 +38,11 @@ namespace XericUI.ReflectionUIGenerator
|
||||
TagIndex = tagIndex;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
public void BindMetaData(AvrgMemberMetaData metaData)
|
||||
{
|
||||
// 新的 Entry 体系通过 Manager.BuildEntries 直接处理此 Attribute
|
||||
// 保留 IXuiavrgAttribute 兼容旧的 SchemaHydration 流程
|
||||
metaData.GroupPath = GroupPath;
|
||||
metaData.Tag = Tag;
|
||||
metaData.TagIndex = TagIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ namespace XericUI.ReflectionUIGenerator
|
||||
Order = order;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
public void BindMetaData(AvrgMemberMetaData metaData)
|
||||
{
|
||||
property.inspectorOrder = Order;
|
||||
metaData.FieldOrder = Order;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -21,16 +21,16 @@ namespace XericUI.ReflectionUIGenerator
|
||||
/// 创建预制体覆盖标记
|
||||
/// </summary>
|
||||
/// <param name="prefabGroupName">
|
||||
/// 目标预制体组名称,与 XuiavrgPropertyGroupScriptableObject.groupName 匹配
|
||||
/// 目标预制体名称,在 AvrgTypePrefabMappingConfig 中查找匹配
|
||||
/// </param>
|
||||
public XuiavrgFieldPrefabOverrideAttribute(string prefabGroupName)
|
||||
{
|
||||
PrefabGroupName = prefabGroupName;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
public void BindMetaData(AvrgMemberMetaData metaData)
|
||||
{
|
||||
// 新的 Entry 体系通过 Manager 直接处理此 Attribute
|
||||
metaData.PrefabOverrideName = PrefabGroupName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-5
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using UnityEngine.Serialization;
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
@@ -20,9 +19,9 @@ namespace XericUI.ReflectionUIGenerator
|
||||
Hide = hideInInspector;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
public void BindMetaData(AvrgMemberMetaData metaData)
|
||||
{
|
||||
property.hideInInspector = Hide;
|
||||
metaData.Hide = Hide;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 不可为空
|
||||
/// </summary>
|
||||
public class XuiavrgNotEmptyAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.notEmpty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89e35e544d66421c8cc524d3b4d7b943
|
||||
timeCreated: 1777360693
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 数值检查
|
||||
/// </summary>
|
||||
public class XuiavrgNumberCheckAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
public readonly float minValue;
|
||||
public readonly float maxValue;
|
||||
|
||||
public XuiavrgNumberCheckAttribute(float minValue, float maxValue)
|
||||
{
|
||||
this.minValue = minValue;
|
||||
this.maxValue = maxValue;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.numberMinValue = minValue;
|
||||
property.numberMaxValue = maxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5b533c6d6ca4de8811adc3913d1fd9b
|
||||
timeCreated: 1777362005
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性检查标记
|
||||
/// 替代旧的 Required/NotEmpty 标记,扩展范围验证、警告、无效值检查
|
||||
///
|
||||
/// 使用示例:
|
||||
/// [XuiavrgPropertyCheck(Required = true)] public string name;
|
||||
/// [XuiavrgPropertyCheck(Required = true, ValidateReasonable = true)] public float speed;
|
||||
/// [XuiavrgPropertyCheck(CheckKey = "MyCustomKey", ValidateWarning = true)] public int score;
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
|
||||
public class XuiavrgPropertyCheckAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断依据标识
|
||||
/// 默认使用类型全称(如 "System.Single")与 AvrgValueRangeConfig 中的条目匹配
|
||||
/// 也可自定义其他名称
|
||||
/// </summary>
|
||||
public string CheckKey { get; set; }
|
||||
|
||||
/// <summary>是否必填</summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>是否启用合理值范围检查</summary>
|
||||
public bool ValidateReasonable { get; set; }
|
||||
|
||||
/// <summary>是否启用警告状态检查</summary>
|
||||
public bool ValidateWarning { get; set; }
|
||||
|
||||
/// <summary>是否启用无效值状态检查</summary>
|
||||
public bool ValidateInvalid { get; set; }
|
||||
|
||||
public XuiavrgPropertyCheckAttribute()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建必填检查标记
|
||||
/// </summary>
|
||||
public XuiavrgPropertyCheckAttribute(bool required)
|
||||
{
|
||||
Required = required;
|
||||
}
|
||||
|
||||
public void BindMetaData(AvrgMemberMetaData metaData)
|
||||
{
|
||||
metaData.Required = Required;
|
||||
|
||||
// 如果指定了 CheckKey,使用它;否则保持元数据默认(元数据默认用 Type.FullName)
|
||||
if (!string.IsNullOrEmpty(CheckKey))
|
||||
metaData.PropertyCheckKey = CheckKey;
|
||||
|
||||
metaData.ValidateReasonable = ValidateReasonable;
|
||||
metaData.ValidateWarning = ValidateWarning;
|
||||
metaData.ValidateInvalid = ValidateInvalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 211cf8030617ad143ba1f4f68f544658
|
||||
guid: 217e7cb9b7af5af4186e2d3f10a89c7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 必填
|
||||
/// </summary>
|
||||
public class XuiavrgRequiredAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.required = true;
|
||||
property.notEmpty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcc4b8b735e84251b11ef87e9fe5b2cb
|
||||
timeCreated: 1777358718
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 字符串检查
|
||||
/// </summary>
|
||||
public class XuiavrgStringCheckAttribute : Attribute, IXuiavrgAttribute
|
||||
{
|
||||
public readonly int minLength;
|
||||
public readonly int maxLength;
|
||||
public readonly string regexPattern;
|
||||
public readonly bool onlyLetters;
|
||||
public readonly bool onlyNumber;
|
||||
public readonly bool onlyEmail;
|
||||
public readonly bool onlyPhone;
|
||||
public readonly bool onlyUrl;
|
||||
|
||||
public XuiavrgStringCheckAttribute(
|
||||
int minLength = 0, int maxLength = int.MaxValue,
|
||||
string regexPattern = "",
|
||||
bool onlyLetters = false, bool onlyNumber = false, bool onlyEmail = true, bool onlyPhone = true,
|
||||
bool onlyUrl = true)
|
||||
{
|
||||
this.minLength = minLength;
|
||||
this.maxLength = maxLength;
|
||||
this.regexPattern = regexPattern;
|
||||
this.onlyLetters = onlyLetters;
|
||||
this.onlyNumber = onlyNumber;
|
||||
this.onlyEmail = onlyEmail;
|
||||
this.onlyPhone = onlyPhone;
|
||||
this.onlyUrl = onlyUrl;
|
||||
}
|
||||
|
||||
public void BindProperty(XuiavrgInspectorProperty property)
|
||||
{
|
||||
property.stringMinLength = minLength;
|
||||
property.stringMaxLength = maxLength;
|
||||
property.stringRegexPattern = regexPattern;
|
||||
property.stringOnlyLetters = onlyLetters;
|
||||
property.stringOnlyNumber = onlyNumber;
|
||||
property.stringOnlyEmail = onlyEmail;
|
||||
property.stringOnlyPhone = onlyPhone;
|
||||
property.stringOnlyUrl = onlyUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0a0e34665ed24a7c87e5eb84a45f6ef6
|
||||
timeCreated: 1777360997
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个成员的验证结果枚举
|
||||
/// </summary>
|
||||
public enum AvrgMemberCheckResult
|
||||
{
|
||||
/// <summary>未标记检查或不适用(跳过)</summary>
|
||||
Skipped = 0,
|
||||
|
||||
/// <summary>检查通过,值有效</summary>
|
||||
Valid = 1,
|
||||
|
||||
/// <summary>必填但为空</summary>
|
||||
Empty = 2,
|
||||
|
||||
/// <summary>值在警告范围内</summary>
|
||||
Warning = 3,
|
||||
|
||||
/// <summary>值在无效范围内</summary>
|
||||
Invalid = 4,
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba7420972b684874dbab8713369d6f1b
|
||||
guid: 51cc10b88f508db4495e8998f44e8b01
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
@@ -0,0 +1,148 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个成员的元数据数据结构
|
||||
/// 记录从反射 + Attribute 标记中提取的所有信息
|
||||
/// </summary>
|
||||
public class AvrgMemberMetaData
|
||||
{
|
||||
// ============ 反射信息 ============
|
||||
|
||||
/// <summary>字段/属性名</summary>
|
||||
public string MemberName { get; set; }
|
||||
|
||||
/// <summary>反射信息 (FieldInfo 或 PropertyInfo)</summary>
|
||||
public MemberInfo MemberInfo { get; set; }
|
||||
|
||||
/// <summary>值类型</summary>
|
||||
public Type ValueType { get; set; }
|
||||
|
||||
/// <summary>是否为属性 (false 为字段)</summary>
|
||||
public bool IsProperty { get; set; }
|
||||
|
||||
/// <summary>绑定的目标实例 (可选,Step1 时可空)</summary>
|
||||
public object TargetInstance { get; set; }
|
||||
|
||||
// ============ 从 IXuiavrgAttribute 融合的元数据 ============
|
||||
|
||||
/// <summary>显示标签</summary>
|
||||
public string LabelName { get; set; }
|
||||
|
||||
/// <summary>单位文本</summary>
|
||||
public string UnitLabelName { get; set; }
|
||||
|
||||
/// <summary>功能标记</summary>
|
||||
public XuiavrgFieldTag Tag { get; set; } = XuiavrgFieldTag.None;
|
||||
|
||||
/// <summary>标记索引 (同 Tag 多实例时区分)</summary>
|
||||
public int TagIndex { get; set; }
|
||||
|
||||
/// <summary>排序值 (越小越靠前)</summary>
|
||||
public int FieldOrder { get; set; }
|
||||
|
||||
/// <summary>编组路径 (以 "/" 分割层级)</summary>
|
||||
public string GroupPath { get; set; }
|
||||
|
||||
/// <summary>预制体覆盖名称</summary>
|
||||
public string PrefabOverrideName { get; set; }
|
||||
|
||||
/// <summary>是否在 UI 中隐藏</summary>
|
||||
public bool Hide { get; set; }
|
||||
|
||||
/// <summary>值变更时触发的反射调用方法名</summary>
|
||||
public string OnTriggerCall { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 类的初始默认值(由类字段初始化器决定)
|
||||
/// 例如 public float speed = 10f; → DefaultValue = 10f
|
||||
/// </summary>
|
||||
public object DefaultValue { get; set; }
|
||||
|
||||
// ============ 属性检查 (由 [PropertyCheck] 填充) ============
|
||||
|
||||
/// <summary>
|
||||
/// 判断依据标识
|
||||
/// 默认使用 ValueType.FullName 与 AvrgValueRangeConfig 中的条目匹配
|
||||
/// </summary>
|
||||
public string PropertyCheckKey { get; set; }
|
||||
|
||||
/// <summary>是否必填</summary>
|
||||
public bool Required { get; set; }
|
||||
|
||||
/// <summary>是否不可为空</summary>
|
||||
public bool NotEmpty { get; set; }
|
||||
|
||||
/// <summary>是否启用合理值范围检查</summary>
|
||||
public bool ValidateReasonable { get; set; }
|
||||
|
||||
/// <summary>是否启用警告状态检查</summary>
|
||||
public bool ValidateWarning { get; set; }
|
||||
|
||||
/// <summary>是否启用无效值状态检查</summary>
|
||||
public bool ValidateInvalid { get; set; }
|
||||
|
||||
// ============ (已弃用字段保留) 旧的验证约束 ============
|
||||
|
||||
/// <summary>字符串最小长度</summary>
|
||||
[Obsolete("将移至 ValueRangeConfig 管理")]
|
||||
public int StringMinLength { get; set; }
|
||||
|
||||
/// <summary>字符串最大长度</summary>
|
||||
public int StringMaxLength { get; set; } = int.MaxValue;
|
||||
|
||||
/// <summary>数值最小值</summary>
|
||||
[Obsolete("将移至 ValueRangeConfig 管理")]
|
||||
public float NumberMinValue { get; set; }
|
||||
|
||||
/// <summary>数值最大值</summary>
|
||||
[Obsolete("将移至 ValueRangeConfig 管理")]
|
||||
public float NumberMaxValue { get; set; }
|
||||
|
||||
// ============ 原始 Attribute 引用 ============
|
||||
|
||||
/// <summary>
|
||||
/// 收集到的所有 IXuiavrgAttribute 列表
|
||||
/// 保留原始引用,供扩展和自定义处理使用
|
||||
/// </summary>
|
||||
public List<IXuiavrgAttribute> Attributes { get; set; } = new List<IXuiavrgAttribute>();
|
||||
|
||||
/// <summary>
|
||||
/// 从绑定目标获取值
|
||||
/// </summary>
|
||||
public object GetValue()
|
||||
{
|
||||
if (TargetInstance == null) return null;
|
||||
switch (MemberInfo)
|
||||
{
|
||||
case PropertyInfo pi:
|
||||
return pi.GetValue(TargetInstance);
|
||||
case FieldInfo fi:
|
||||
return fi.GetValue(TargetInstance);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值到绑定目标
|
||||
/// </summary>
|
||||
public void SetValue(object value)
|
||||
{
|
||||
if (TargetInstance == null) return;
|
||||
switch (MemberInfo)
|
||||
{
|
||||
case PropertyInfo pi:
|
||||
if (pi.CanWrite)
|
||||
pi.SetValue(TargetInstance, value);
|
||||
break;
|
||||
case FieldInfo fi:
|
||||
fi.SetValue(TargetInstance, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f152de979aea6de48a4979fa509ac23f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 元数据集
|
||||
/// 包含从对象反射提取的完整元数据,以及用于生成 MVVM 绑定的 BuildMDVM() 方法
|
||||
/// </summary>
|
||||
public class AvrgMemberMetaDataSet
|
||||
{
|
||||
private List<AvrgMemberMetaData> _metaDataList = new List<AvrgMemberMetaData>();
|
||||
|
||||
/// <summary>
|
||||
/// 只读访问元数据列表
|
||||
/// </summary>
|
||||
public IReadOnlyList<AvrgMemberMetaData> MetaDataList => _metaDataList;
|
||||
|
||||
/// <summary>
|
||||
/// 添加元数据
|
||||
/// </summary>
|
||||
public void Add(AvrgMemberMetaData metaData)
|
||||
{
|
||||
_metaDataList.Add(metaData);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理编组合并 — 将 GroupPath 相同的元数据合并
|
||||
/// 非编组元数据保持为独立 Entry
|
||||
/// </summary>
|
||||
internal void MergeGroupEntries()
|
||||
{
|
||||
// 提取编组和非编组
|
||||
var grouped = _metaDataList
|
||||
.Where(m => !string.IsNullOrEmpty(m.GroupPath))
|
||||
.GroupBy(m => m.GroupPath)
|
||||
.ToList();
|
||||
|
||||
var nonGrouped = _metaDataList
|
||||
.Where(m => string.IsNullOrEmpty(m.GroupPath))
|
||||
.ToList();
|
||||
|
||||
var merged = new List<AvrgMemberMetaData>();
|
||||
|
||||
foreach (var gEntry in grouped)
|
||||
{
|
||||
// 编组内第一个成员作为主元数据
|
||||
var first = gEntry.First();
|
||||
// 合成的 metaData 保持 GroupPath 和主 Tag
|
||||
merged.Add(first);
|
||||
}
|
||||
|
||||
// 非编组直接追加
|
||||
merged.AddRange(nonGrouped);
|
||||
|
||||
_metaDataList = merged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 FieldOrder 排序
|
||||
/// </summary>
|
||||
internal void SortByFieldOrder()
|
||||
{
|
||||
_metaDataList.Sort((a, b) =>
|
||||
{
|
||||
int orderCompare = a.FieldOrder.CompareTo(b.FieldOrder);
|
||||
if (orderCompare != 0) return orderCompare;
|
||||
return string.Compare(a.MemberName, b.MemberName, StringComparison.Ordinal);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为元数据生成对应的 MVVM 值绑定模板
|
||||
/// </summary>
|
||||
/// <param name="businessTarget">可选的业务目标对象(如果元数据中没有绑定目标,则在此传入)</param>
|
||||
/// <param name="resolverConfig">可选的冲突解决配置</param>
|
||||
/// <returns>绑定集合</returns>
|
||||
public AvrgMvvmBindingCollection BuildMDVM(
|
||||
object businessTarget = null,
|
||||
AvrgMvvmResolverConfig resolverConfig = null)
|
||||
{
|
||||
var bindingCollection = new AvrgMvvmBindingCollection();
|
||||
var scheduler = new AvrgMvvmScheduler(resolverConfig);
|
||||
|
||||
foreach (var metaData in _metaDataList)
|
||||
{
|
||||
// 确定目标实例
|
||||
var target = metaData.TargetInstance ?? businessTarget;
|
||||
if (target == null)
|
||||
continue;
|
||||
|
||||
// 创建绑定
|
||||
var binding = new AvrgMvvmBinding
|
||||
{
|
||||
MetaData = metaData,
|
||||
ReadFunc = () => metaData.GetValue(),
|
||||
};
|
||||
|
||||
var capturedBinding = binding;
|
||||
binding.WriteAction = (val) =>
|
||||
{
|
||||
metaData.SetValue(val);
|
||||
scheduler.MarkDataDirty(capturedBinding);
|
||||
};
|
||||
|
||||
// 注册到调度器
|
||||
scheduler.Register(binding);
|
||||
bindingCollection.Add(binding);
|
||||
}
|
||||
|
||||
bindingCollection.AttachScheduler(scheduler);
|
||||
return bindingCollection;
|
||||
}
|
||||
|
||||
// ============ 验证方法 ============
|
||||
|
||||
/// <summary>
|
||||
/// 对当前元数据集执行属性检查验证
|
||||
/// </summary>
|
||||
/// <param name="valueRangeConfig">值范围配置(可选,不传入则不执行范围检查)</param>
|
||||
/// <returns>验证结果集合</returns>
|
||||
public AvrgValidationResultSet Validate(AvrgValueRangeConfig valueRangeConfig = null)
|
||||
{
|
||||
var resultSet = new AvrgValidationResultSet();
|
||||
|
||||
foreach (var metaData in _metaDataList)
|
||||
{
|
||||
if (metaData.Hide)
|
||||
continue;
|
||||
|
||||
// 只有标记了 PropertyCheck 的才验证
|
||||
if (!metaData.Required && !metaData.ValidateReasonable
|
||||
&& !metaData.ValidateWarning && !metaData.ValidateInvalid)
|
||||
{
|
||||
resultSet.Results.Add(new AvrgMemberValidationResult
|
||||
{
|
||||
MetaData = metaData,
|
||||
Result = AvrgMemberCheckResult.Skipped,
|
||||
CurrentValueString = metaData.GetValue()?.ToString(),
|
||||
Message = "未标记属性检查",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = metaData.GetValue();
|
||||
var checkResult = CheckSingleMember(metaData, value, valueRangeConfig);
|
||||
resultSet.Results.Add(checkResult);
|
||||
}
|
||||
|
||||
return resultSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证单个成员
|
||||
/// </summary>
|
||||
private AvrgMemberValidationResult CheckSingleMember(
|
||||
AvrgMemberMetaData metaData,
|
||||
object value,
|
||||
AvrgValueRangeConfig rangeConfig)
|
||||
{
|
||||
var result = new AvrgMemberValidationResult
|
||||
{
|
||||
MetaData = metaData,
|
||||
CurrentValueString = value?.ToString() ?? "(null)",
|
||||
};
|
||||
|
||||
// 根据 CheckKey 查找值范围规则
|
||||
AvrgValueRangeEntry rangeEntry = null;
|
||||
string checkKey = !string.IsNullOrEmpty(metaData.PropertyCheckKey)
|
||||
? metaData.PropertyCheckKey
|
||||
: metaData.ValueType?.FullName;
|
||||
|
||||
if (rangeConfig != null && !string.IsNullOrEmpty(checkKey))
|
||||
{
|
||||
rangeEntry = rangeConfig.FindEntry(checkKey);
|
||||
}
|
||||
|
||||
// ——— 优先级 1: 空值检查 (Required) ———
|
||||
if (metaData.Required)
|
||||
{
|
||||
bool isEmpty = false;
|
||||
|
||||
// 引用类型 null
|
||||
if (value == null)
|
||||
{
|
||||
isEmpty = true;
|
||||
}
|
||||
// 字符串 null/空
|
||||
else if (value is string str)
|
||||
{
|
||||
isEmpty = string.IsNullOrEmpty(str);
|
||||
}
|
||||
// 使用 ValueRangeEntry 的 IsEmpty 判断
|
||||
else if (rangeEntry != null && !rangeEntry.skipEmptyCheck)
|
||||
{
|
||||
isEmpty = rangeEntry.IsEmpty(value);
|
||||
}
|
||||
|
||||
if (isEmpty)
|
||||
{
|
||||
result.Result = AvrgMemberCheckResult.Empty;
|
||||
result.Message = $"必填字段 '{metaData.MemberName}' 为空";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有范围配置,无法继续检查
|
||||
if (rangeEntry == null)
|
||||
{
|
||||
result.Result = AvrgMemberCheckResult.Valid;
|
||||
result.Message = "通过(无范围配置)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ——— 优先级 2: 合理值检查(最高优先级,命中即通过) ———
|
||||
if (metaData.ValidateReasonable && rangeEntry.IsReasonable(value))
|
||||
{
|
||||
result.Result = AvrgMemberCheckResult.Valid;
|
||||
result.Message = "值在合理范围内";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ——— 优先级 3: 无效值检查 ———
|
||||
if (metaData.ValidateInvalid && rangeEntry.IsInvalid(value))
|
||||
{
|
||||
result.Result = AvrgMemberCheckResult.Invalid;
|
||||
result.Message = $"值 {value} 在无效范围内";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ——— 优先级 4: 警告值检查 ———
|
||||
if (metaData.ValidateWarning && rangeEntry.IsWarning(value))
|
||||
{
|
||||
result.Result = AvrgMemberCheckResult.Warning;
|
||||
result.Message = $"值 {value} 在警告范围内";
|
||||
return result;
|
||||
}
|
||||
|
||||
// ——— 最终: 通过 ———
|
||||
result.Result = AvrgMemberCheckResult.Valid;
|
||||
result.Message = "通过";
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9382162bffd9c7c43ac24fca5ca83cb0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,69 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个成员的详细验证结果
|
||||
/// </summary>
|
||||
public struct AvrgMemberValidationResult
|
||||
{
|
||||
/// <summary>验证的元数据</summary>
|
||||
public AvrgMemberMetaData MetaData { get; set; }
|
||||
|
||||
/// <summary>验证结果</summary>
|
||||
public AvrgMemberCheckResult Result { get; set; }
|
||||
|
||||
/// <summary>当前值(字符串形式,便于显示)</summary>
|
||||
public string CurrentValueString { get; set; }
|
||||
|
||||
/// <summary>验证消息(说明原因)</summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>是否通过验证</summary>
|
||||
public bool IsPassed => Result == AvrgMemberCheckResult.Valid
|
||||
|| Result == AvrgMemberCheckResult.Skipped
|
||||
|| Result == AvrgMemberCheckResult.Warning; // Warning 视为通过但需注意
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 完整验证结果集合
|
||||
/// </summary>
|
||||
public class AvrgValidationResultSet
|
||||
{
|
||||
/// <summary>所有成员的验证结果</summary>
|
||||
public List<AvrgMemberValidationResult> Results { get; set; } = new List<AvrgMemberValidationResult>();
|
||||
|
||||
/// <summary>未验证通过的条目数(Empty + Invalid)</summary>
|
||||
public int FailedCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var r in Results)
|
||||
{
|
||||
if (r.Result == AvrgMemberCheckResult.Empty || r.Result == AvrgMemberCheckResult.Invalid)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>警告条目数</summary>
|
||||
public int WarningCount
|
||||
{
|
||||
get
|
||||
{
|
||||
int count = 0;
|
||||
foreach (var r in Results)
|
||||
{
|
||||
if (r.Result == AvrgMemberCheckResult.Warning)
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>是否全部通过(无失败项)</summary>
|
||||
public bool IsAllPassed => FailedCount == 0;
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb4d50aa593365b4abae59c7ac2ae557
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 值绑定模板
|
||||
/// 为单个元数据成员创建的 MVVM 双向绑定,包含读/写委托和脏标记
|
||||
/// </summary>
|
||||
public class AvrgMvvmBinding
|
||||
{
|
||||
/// <summary>关联的元数据</summary>
|
||||
public AvrgMemberMetaData MetaData { get; set; }
|
||||
|
||||
/// <summary>从数据源读取值的委托</summary>
|
||||
public Func<object> ReadFunc { get; set; }
|
||||
|
||||
/// <summary>写入值到数据源的委托</summary>
|
||||
public Action<object> WriteAction { get; set; }
|
||||
|
||||
// ============ 脏标记 ============
|
||||
|
||||
/// <summary>UI 侧是否标记为脏</summary>
|
||||
public bool IsUIDirty { get; set; }
|
||||
|
||||
/// <summary>数据侧是否标记为脏</summary>
|
||||
public bool IsDataDirty { get; set; }
|
||||
|
||||
/// <summary>UI 侧写入的缓存值</summary>
|
||||
public object UICachedValue { get; set; }
|
||||
|
||||
/// <summary>数据侧写入的缓存值</summary>
|
||||
public object DataCachedValue { get; set; }
|
||||
|
||||
// ============ UI 绑定 ============
|
||||
|
||||
/// <summary>UI 值变更回调委托 — UI 组件订阅此回调以更新显示</summary>
|
||||
public Action<object> OnUIDataChanged;
|
||||
|
||||
/// <summary>通知 UI 更新值</summary>
|
||||
public void NotifyUIUpdate(object value)
|
||||
{
|
||||
OnUIDataChanged?.Invoke(value);
|
||||
}
|
||||
|
||||
/// <summary>清除所有 UI 变更订阅</summary>
|
||||
public void ClearUIEvent()
|
||||
{
|
||||
OnUIDataChanged = null;
|
||||
}
|
||||
|
||||
// ============ 便捷方法 ============
|
||||
|
||||
/// <summary>
|
||||
/// UI 侧写入值(标记脏,不立即传播)
|
||||
/// </summary>
|
||||
public void WriteFromUI(object value)
|
||||
{
|
||||
UICachedValue = value;
|
||||
IsUIDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据侧写入值(标记脏,不立即传播)
|
||||
/// </summary>
|
||||
public void WriteFromData(object value)
|
||||
{
|
||||
DataCachedValue = value;
|
||||
IsDataDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据源读取当前值
|
||||
/// </summary>
|
||||
public object ReadFromDataSource()
|
||||
{
|
||||
return ReadFunc?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将值实际写入数据源
|
||||
/// </summary>
|
||||
public void CommitToDataSource(object value)
|
||||
{
|
||||
WriteAction?.Invoke(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置脏标记
|
||||
/// </summary>
|
||||
public void ClearDirty()
|
||||
{
|
||||
IsUIDirty = false;
|
||||
IsDataDirty = false;
|
||||
UICachedValue = null;
|
||||
DataCachedValue = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d011f5c07bb55648b27e40166a1ca58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,82 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 绑定集合 — 统一管理所有 AvrgMvvmBinding
|
||||
/// </summary>
|
||||
public class AvrgMvvmBindingCollection : IEnumerable<AvrgMvvmBinding>
|
||||
{
|
||||
private List<AvrgMvvmBinding> _bindings = new List<AvrgMvvmBinding>();
|
||||
private AvrgMvvmScheduler _scheduler;
|
||||
|
||||
/// <summary>
|
||||
/// 绑定的元数据集引用(可选)
|
||||
/// </summary>
|
||||
public AvrgMemberMetaDataSet SourceMetaDataSet { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 绑定数量
|
||||
/// </summary>
|
||||
public int Count => _bindings.Count;
|
||||
|
||||
/// <summary>
|
||||
/// 添加绑定
|
||||
/// </summary>
|
||||
public void Add(AvrgMvvmBinding binding)
|
||||
{
|
||||
_bindings.Add(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定索引的绑定
|
||||
/// </summary>
|
||||
public AvrgMvvmBinding this[int index] => _bindings[index];
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定成员名称的绑定
|
||||
/// </summary>
|
||||
public AvrgMvvmBinding FindByMemberName(string memberName)
|
||||
{
|
||||
return _bindings.Find(b => b.MetaData?.MemberName == memberName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定 Tag 类型的绑定列表
|
||||
/// </summary>
|
||||
public List<AvrgMvvmBinding> FindByTag(XuiavrgFieldTag tag)
|
||||
{
|
||||
return _bindings.FindAll(b => (b.MetaData?.Tag & tag) == tag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 附加调度器
|
||||
/// </summary>
|
||||
internal void AttachScheduler(AvrgMvvmScheduler scheduler)
|
||||
{
|
||||
_scheduler = scheduler;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 触发调度器 Flush,处理所有脏标记
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
_scheduler?.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有绑定
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_bindings.Clear();
|
||||
_scheduler?.Clear();
|
||||
}
|
||||
|
||||
public IEnumerator<AvrgMvvmBinding> GetEnumerator() => _bindings.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2cd947ac831d21b4288a0b3268d79ffd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 可配置的冲突解决器 ScriptableObject
|
||||
/// 预配置各种冲突场景下的决策方法,以及可注册的自定义解析器列表
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "Xeric UI avr Resolver Config",
|
||||
menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Resolver Config",
|
||||
order = 20)]
|
||||
public class AvrgMvvmResolverConfig : ScriptableObject, IAvrgMvvmConflictResolver
|
||||
{
|
||||
/// <summary>全局默认冲突解决策略</summary>
|
||||
[Tooltip("全局默认冲突解决策略")]
|
||||
public ConflictResolutionStrategy defaultStrategy = ConflictResolutionStrategy.PreferUI;
|
||||
|
||||
/// <summary>自定义解析器列表(按序尝试)</summary>
|
||||
[Tooltip("自定义解析器类型全名列表(按序尝试,需实现 IAvrgMvvmConflictResolver)")]
|
||||
public List<string> customResolverTypeNames = new List<string>();
|
||||
|
||||
// 运行时已实例化的解析器缓存
|
||||
[NonSerialized]
|
||||
private List<IAvrgMvvmConflictResolver> _resolverInstances;
|
||||
|
||||
/// <summary>
|
||||
/// 获取或实例化自定义解析器
|
||||
/// </summary>
|
||||
private List<IAvrgMvvmConflictResolver> GetResolverInstances()
|
||||
{
|
||||
if (_resolverInstances != null)
|
||||
return _resolverInstances;
|
||||
|
||||
_resolverInstances = new List<IAvrgMvvmConflictResolver>();
|
||||
|
||||
foreach (var typeName in customResolverTypeNames)
|
||||
{
|
||||
try
|
||||
{
|
||||
var type = Type.GetType(typeName);
|
||||
if (type != null && typeof(IAvrgMvvmConflictResolver).IsAssignableFrom(type))
|
||||
{
|
||||
var instance = Activator.CreateInstance(type) as IAvrgMvvmConflictResolver;
|
||||
if (instance != null)
|
||||
_resolverInstances.Add(instance);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogWarning($"[AvrgMvvmResolverConfig] 无法实例化解析器 {typeName}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return _resolverInstances;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试按配置解决冲突
|
||||
/// </summary>
|
||||
public bool TryResolve(AvrgMvvmBinding binding, out object resolvedValue)
|
||||
{
|
||||
// 按序尝试自定义解析器
|
||||
var resolvers = GetResolverInstances();
|
||||
foreach (var resolver in resolvers)
|
||||
{
|
||||
if (resolver.TryResolve(binding, defaultStrategy, out resolvedValue))
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IAvrgMvvmConflictResolver 接口实现
|
||||
/// </summary>
|
||||
bool IAvrgMvvmConflictResolver.TryResolve(
|
||||
AvrgMvvmBinding binding,
|
||||
ConflictResolutionStrategy defaultStrategy,
|
||||
out object resolvedValue)
|
||||
{
|
||||
return TryResolve(binding, out resolvedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e2b7e3ef1b0932a4a9f2cbbb1ae11546
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// MVVM 调度器 — 管理脏标记、触发 Flush 更新、检测并解决冲突
|
||||
///
|
||||
/// 冲突解决策略:
|
||||
/// - 仅数据脏 → 通知 UI 更新
|
||||
/// - 仅 UI 脏 → 写入数据源
|
||||
/// - 双方都脏 → 走配置的冲突解决策略
|
||||
/// </summary>
|
||||
public class AvrgMvvmScheduler
|
||||
{
|
||||
/// <summary>用户可注册的外部冲突解决事件</summary>
|
||||
public event Func<AvrgMvvmBinding, ConflictResolutionStrategy, object> OnConflictDetected;
|
||||
|
||||
/// <summary>当前配置的冲突解决策略</summary>
|
||||
public ConflictResolutionStrategy DefaultStrategy { get; set; } = ConflictResolutionStrategy.PreferUI;
|
||||
|
||||
private List<AvrgMvvmBinding> _bindings = new List<AvrgMvvmBinding>();
|
||||
private HashSet<AvrgMvvmBinding> _dirtySet = new HashSet<AvrgMvvmBinding>();
|
||||
private AvrgMvvmResolverConfig _resolverConfig;
|
||||
|
||||
public AvrgMvvmScheduler(AvrgMvvmResolverConfig resolverConfig = null)
|
||||
{
|
||||
_resolverConfig = resolverConfig;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册绑定到调度器
|
||||
/// </summary>
|
||||
public void Register(AvrgMvvmBinding binding)
|
||||
{
|
||||
if (!_bindings.Contains(binding))
|
||||
_bindings.Add(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 侧写入时调用 — 标记脏
|
||||
/// </summary>
|
||||
public void MarkUIDirty(AvrgMvvmBinding binding)
|
||||
{
|
||||
binding.IsUIDirty = true;
|
||||
_dirtySet.Add(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 数据侧写入时调用 — 标记脏
|
||||
/// </summary>
|
||||
public void MarkDataDirty(AvrgMvvmBinding binding)
|
||||
{
|
||||
binding.IsDataDirty = true;
|
||||
_dirtySet.Add(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理所有脏绑定
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
if (_dirtySet.Count == 0) return;
|
||||
|
||||
foreach (var binding in _dirtySet)
|
||||
{
|
||||
ResolveBinding(binding);
|
||||
binding.ClearDirty();
|
||||
}
|
||||
|
||||
_dirtySet.Clear();
|
||||
}
|
||||
|
||||
private void ResolveBinding(AvrgMvvmBinding binding)
|
||||
{
|
||||
bool uiDirty = binding.IsUIDirty;
|
||||
bool dataDirty = binding.IsDataDirty;
|
||||
|
||||
if (uiDirty && dataDirty)
|
||||
{
|
||||
// 双方都脏 — 走冲突解决
|
||||
ResolveConflict(binding);
|
||||
}
|
||||
else if (dataDirty)
|
||||
{
|
||||
// 仅数据侧更新 → 通知 UI
|
||||
var value = binding.DataCachedValue ?? binding.ReadFromDataSource();
|
||||
binding.CommitToDataSource(value);
|
||||
binding.NotifyUIUpdate(value);
|
||||
}
|
||||
else if (uiDirty)
|
||||
{
|
||||
// 仅 UI 侧更新 → 写入数据源
|
||||
binding.CommitToDataSource(binding.UICachedValue);
|
||||
}
|
||||
}
|
||||
|
||||
private void ResolveConflict(AvrgMvvmBinding binding)
|
||||
{
|
||||
// 1. 检查外部事件处理
|
||||
if (OnConflictDetected != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resolvedValue = OnConflictDetected.Invoke(binding, DefaultStrategy);
|
||||
if (resolvedValue != null)
|
||||
{
|
||||
binding.CommitToDataSource(resolvedValue);
|
||||
binding.NotifyUIUpdate(resolvedValue);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[AvrgMvvmScheduler] 外部冲突解决事件异常: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 检查 ResolverConfig 中的自定义策略
|
||||
if (_resolverConfig != null && _resolverConfig.TryResolve(binding, out object configResolvedValue))
|
||||
{
|
||||
binding.CommitToDataSource(configResolvedValue);
|
||||
binding.NotifyUIUpdate(configResolvedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 按默认策略解决
|
||||
switch (DefaultStrategy)
|
||||
{
|
||||
case ConflictResolutionStrategy.PreferUI:
|
||||
binding.CommitToDataSource(binding.UICachedValue);
|
||||
binding.NotifyUIUpdate(binding.UICachedValue);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.PreferData:
|
||||
var dataValue = binding.DataCachedValue ?? binding.ReadFromDataSource();
|
||||
binding.CommitToDataSource(dataValue);
|
||||
binding.NotifyUIUpdate(dataValue);
|
||||
break;
|
||||
|
||||
case ConflictResolutionStrategy.CompareValues:
|
||||
// 比较UI值和数据值
|
||||
object uiVal = binding.UICachedValue;
|
||||
object dataVal = binding.DataCachedValue ?? binding.ReadFromDataSource();
|
||||
|
||||
if (Equals(uiVal, dataVal))
|
||||
{
|
||||
// 值相同,忽略冲突
|
||||
binding.NotifyUIUpdate(dataVal);
|
||||
break;
|
||||
}
|
||||
|
||||
// 值不同,回退到 PreferUI
|
||||
binding.CommitToDataSource(binding.UICachedValue);
|
||||
binding.NotifyUIUpdate(binding.UICachedValue);
|
||||
Debug.LogWarning($"[AvrgMvvmScheduler] 值冲突(CompareValues回退PreferUI): {binding.MetaData?.MemberName}");
|
||||
break;
|
||||
|
||||
default:
|
||||
binding.CommitToDataSource(binding.UICachedValue);
|
||||
binding.NotifyUIUpdate(binding.UICachedValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有注册
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_bindings.Clear();
|
||||
_dirtySet.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动将绑定的 UI 值标记为脏
|
||||
/// </summary>
|
||||
public void MarkUI(AvrgMvvmBinding binding, object value)
|
||||
{
|
||||
binding.WriteFromUI(value);
|
||||
MarkUIDirty(binding);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 手动将绑定的数据值写入并标记脏
|
||||
/// </summary>
|
||||
public void MarkData(AvrgMvvmBinding binding, object value)
|
||||
{
|
||||
binding.WriteFromData(value);
|
||||
MarkDataDirty(binding);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 冲突解决策略枚举
|
||||
/// </summary>
|
||||
public enum ConflictResolutionStrategy
|
||||
{
|
||||
/// <summary>优先使用 UI 侧的值</summary>
|
||||
PreferUI,
|
||||
|
||||
/// <summary>优先使用数据源侧的值</summary>
|
||||
PreferData,
|
||||
|
||||
/// <summary>比较两者,相同则忽略,不同则优先 UI</summary>
|
||||
CompareValues,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c0f505fa03fa4e74399ff1e62540dfd8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型-预制体映射配置 ScriptableObject
|
||||
/// 用于在 GenerateUI 阶段根据数据类型查找匹配的预制体
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "Xeric UI avr Type-Prefab Mapping",
|
||||
menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Type-Prefab Mapping",
|
||||
order = 15)]
|
||||
public class AvrgTypePrefabMappingConfig : ScriptableObject
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个类型映射条目
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class TypePrefabEntry
|
||||
{
|
||||
[Tooltip("C# 数据类型全名 (如 System.Single, System.String, System.Boolean)")]
|
||||
public string valueTypeName;
|
||||
|
||||
[Tooltip("对应的 UI 预制体")]
|
||||
public GameObject prefab;
|
||||
|
||||
/// <summary>缓存的类型</summary>
|
||||
[NonSerialized] public Type cachedType;
|
||||
}
|
||||
|
||||
/// <summary>类型映射列表</summary>
|
||||
[Tooltip("类型映射列表:数据类型 → UI 预制体")]
|
||||
public List<TypePrefabEntry> typePrefabEntries = new List<TypePrefabEntry>();
|
||||
|
||||
/// <summary>默认预制体(无精确匹配时的回退)</summary>
|
||||
[Tooltip("默认预制体(没有类型匹配时的回退方案)")]
|
||||
public GameObject defaultPrefab;
|
||||
|
||||
[NonSerialized]
|
||||
private Dictionary<Type, GameObject> _typeCache;
|
||||
|
||||
private void BuildCache()
|
||||
{
|
||||
if (_typeCache != null) return;
|
||||
_typeCache = new Dictionary<Type, GameObject>();
|
||||
|
||||
foreach (var entry in typePrefabEntries)
|
||||
{
|
||||
if (entry.prefab == null) continue;
|
||||
|
||||
if (entry.cachedType == null && !string.IsNullOrEmpty(entry.valueTypeName))
|
||||
entry.cachedType = Type.GetType(entry.valueTypeName);
|
||||
|
||||
if (entry.cachedType != null && !_typeCache.ContainsKey(entry.cachedType))
|
||||
_typeCache[entry.cachedType] = entry.prefab;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据数据类型查找匹配的预制体
|
||||
/// </summary>
|
||||
public GameObject FindPrefabByType(Type valueType)
|
||||
{
|
||||
if (valueType == null) return defaultPrefab;
|
||||
|
||||
BuildCache();
|
||||
|
||||
if (_typeCache.TryGetValue(valueType, out var prefab))
|
||||
return prefab;
|
||||
|
||||
// 精确匹配失败,尝试基类/接口匹配
|
||||
foreach (var kvp in _typeCache)
|
||||
{
|
||||
if (kvp.Key.IsAssignableFrom(valueType))
|
||||
return kvp.Value;
|
||||
}
|
||||
|
||||
return defaultPrefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据绑定的 Entry Bindings 查找匹配的预制体
|
||||
/// </summary>
|
||||
public GameObject FindPrefabByBindings(IReadOnlyList<AvrgMvvmBinding> bindings)
|
||||
{
|
||||
if (bindings == null || bindings.Count == 0) return defaultPrefab;
|
||||
|
||||
var metaData = bindings[0].MetaData;
|
||||
if (metaData?.ValueType == null) return defaultPrefab;
|
||||
|
||||
// 优先按 PrefabOverrideName 查找预制体
|
||||
if (!string.IsNullOrEmpty(metaData.PrefabOverrideName))
|
||||
{
|
||||
foreach (var entry in typePrefabEntries)
|
||||
{
|
||||
if (entry.prefab != null && entry.prefab.name == metaData.PrefabOverrideName)
|
||||
return entry.prefab;
|
||||
}
|
||||
}
|
||||
|
||||
// 按类型匹配
|
||||
var typeMatch = FindPrefabByType(metaData.ValueType);
|
||||
if (typeMatch != null) return typeMatch;
|
||||
|
||||
return defaultPrefab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按预制体名称查找
|
||||
/// </summary>
|
||||
public GameObject FindPrefabByName(string prefabName)
|
||||
{
|
||||
foreach (var entry in typePrefabEntries)
|
||||
{
|
||||
if (entry.prefab != null && entry.prefab.name == prefabName)
|
||||
return entry.prefab;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空缓存
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_typeCache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31a97ff1112e7a84e9ade9ca81ea19a2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 值范围配置 ScriptableObject
|
||||
/// 定义不同类型的空/无效/警告/合理值范围判断依据
|
||||
/// </summary>
|
||||
[CreateAssetMenu(
|
||||
fileName = "Xeric UI avr Value Range Config",
|
||||
menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Value Range Config",
|
||||
order = 25)]
|
||||
public class AvrgValueRangeConfig : ScriptableObject
|
||||
{
|
||||
/// <summary>值范围规则列表</summary>
|
||||
[Tooltip("按类型定义的取值范围规则")]
|
||||
public List<AvrgValueRangeEntry> entries = new List<AvrgValueRangeEntry>();
|
||||
|
||||
[NonSerialized]
|
||||
private Dictionary<string, AvrgValueRangeEntry> _entryCache;
|
||||
|
||||
private void BuildCache()
|
||||
{
|
||||
if (_entryCache != null) return;
|
||||
_entryCache = new Dictionary<string, AvrgValueRangeEntry>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(entry.key) && !_entryCache.ContainsKey(entry.key))
|
||||
_entryCache[entry.key] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据键查找值范围规则
|
||||
/// </summary>
|
||||
/// <param name="key">类型标识(通常为 Type.FullName)</param>
|
||||
/// <returns>匹配的规则,未找到则返回 null</returns>
|
||||
public AvrgValueRangeEntry FindEntry(string key)
|
||||
{
|
||||
if (string.IsNullOrEmpty(key)) return null;
|
||||
BuildCache();
|
||||
_entryCache.TryGetValue(key, out var entry);
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Type 查找值范围规则
|
||||
/// </summary>
|
||||
public AvrgValueRangeEntry FindEntryByType(Type type)
|
||||
{
|
||||
if (type == null) return null;
|
||||
return FindEntry(type.FullName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空缓存(用于编辑器热重载后重新构建)
|
||||
/// </summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
_entryCache = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a432178c6615374f89344e6e53c75dd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,164 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 值范围规则条目
|
||||
/// 定义某个类型(由 CheckKey 标识)的空/无效/警告/合理值范围
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AvrgValueRangeEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 类型标识符(默认使用 Type.FullName,如 "System.Single", "System.Int32")
|
||||
/// 也支持自定义字符串以匹配特定语义
|
||||
/// </summary>
|
||||
public string key = "";
|
||||
|
||||
// ===== 空值范围 =====
|
||||
|
||||
/// <summary>
|
||||
/// 是否跳过空值检查(如 bool 类型无法判断空)
|
||||
/// </summary>
|
||||
public bool skipEmptyCheck = false;
|
||||
|
||||
/// <summary>
|
||||
/// 视为空的最小值(>= 此值视为空)
|
||||
/// </summary>
|
||||
public float emptyRangeMin = float.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 视为空的最大值(<= 此值视为空)
|
||||
/// </summary>
|
||||
public float emptyRangeMax = float.MaxValue;
|
||||
|
||||
// ===== 合理值范围(最高优先级) =====
|
||||
|
||||
/// <summary>
|
||||
/// 合理值最小值
|
||||
/// </summary>
|
||||
public float reasonableRangeMin = float.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 合理值最大值
|
||||
/// </summary>
|
||||
public float reasonableRangeMax = float.MaxValue;
|
||||
|
||||
// ===== 无效值范围 =====
|
||||
|
||||
/// <summary>
|
||||
/// 无效值最小值
|
||||
/// </summary>
|
||||
public float invalidRangeMin = float.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 无效值最大值
|
||||
/// </summary>
|
||||
public float invalidRangeMax = float.MaxValue;
|
||||
|
||||
// ===== 警告值范围 =====
|
||||
|
||||
/// <summary>
|
||||
/// 警告值最小值
|
||||
/// </summary>
|
||||
public float warningRangeMin = float.MinValue;
|
||||
|
||||
/// <summary>
|
||||
/// 警告值最大值
|
||||
/// </summary>
|
||||
public float warningRangeMax = float.MaxValue;
|
||||
|
||||
// ===== 字符串相关 =====
|
||||
|
||||
/// <summary>
|
||||
/// 警告关键词列表(仅字符串类型使用)
|
||||
/// </summary>
|
||||
public List<string> warningKeywords = new List<string>();
|
||||
|
||||
// ===== 便捷判断方法 =====
|
||||
|
||||
/// <summary>
|
||||
/// 将值转为 float 进行比较(bool→0/1, 数值→直接转, 字符串→长度)
|
||||
/// </summary>
|
||||
public static float ToComparableFloat(object value)
|
||||
{
|
||||
if (value == null) return float.NaN;
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case float f: return f;
|
||||
case double d: return (float)d;
|
||||
case int i: return i;
|
||||
case long l: return l;
|
||||
case short s: return s;
|
||||
case byte b: return b;
|
||||
case uint ui: return ui;
|
||||
case bool bVal: return bVal ? 1f : 0f;
|
||||
case string str: return str.Length;
|
||||
default: return float.NaN;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断值是否为空
|
||||
/// </summary>
|
||||
public bool IsEmpty(object value)
|
||||
{
|
||||
if (skipEmptyCheck) return false;
|
||||
|
||||
// 引用类型 null
|
||||
if (value == null) return true;
|
||||
|
||||
// 字符串 null/空
|
||||
if (value is string str) return string.IsNullOrEmpty(str);
|
||||
|
||||
// 数值范围判断
|
||||
float f = ToComparableFloat(value);
|
||||
if (float.IsNaN(f)) return false; // 无法判断的类型跳过
|
||||
|
||||
return f >= emptyRangeMin && f <= emptyRangeMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断值是否在合理范围内
|
||||
/// </summary>
|
||||
public bool IsReasonable(object value)
|
||||
{
|
||||
float f = ToComparableFloat(value);
|
||||
if (float.IsNaN(f)) return false;
|
||||
return f >= reasonableRangeMin && f <= reasonableRangeMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断值是否在无效范围内
|
||||
/// </summary>
|
||||
public bool IsInvalid(object value)
|
||||
{
|
||||
float f = ToComparableFloat(value);
|
||||
if (float.IsNaN(f)) return false;
|
||||
return f >= invalidRangeMin && f <= invalidRangeMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断值是否在警告范围内
|
||||
/// </summary>
|
||||
public bool IsWarning(object value)
|
||||
{
|
||||
// 先检查关键词(字符串)
|
||||
if (value is string str && warningKeywords.Count > 0)
|
||||
{
|
||||
foreach (var kw in warningKeywords)
|
||||
{
|
||||
if (str.Contains(kw, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 数值范围
|
||||
float f = ToComparableFloat(value);
|
||||
if (float.IsNaN(f)) return false;
|
||||
return f >= warningRangeMin && f <= warningRangeMax;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3e5066f03b4a03a4f8b2993f027f713b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 冲突解决器接口
|
||||
/// 用户可实现此接口注册自定义冲突解决逻辑
|
||||
/// </summary>
|
||||
public interface IAvrgMvvmConflictResolver
|
||||
{
|
||||
/// <summary>
|
||||
/// 尝试解决冲突
|
||||
/// </summary>
|
||||
/// <param name="binding">发生冲突的绑定</param>
|
||||
/// <param name="defaultStrategy">当前配置的默认策略</param>
|
||||
/// <param name="resolvedValue">返回解决后的值</param>
|
||||
/// <returns>true 表示已解决,false 表示未处理(回退到默认策略)</returns>
|
||||
bool TryResolve(AvrgMvvmBinding binding, ConflictResolutionStrategy defaultStrategy, out object resolvedValue);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c4e7c4a0ce16c64082ee641e03ce5d0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -1,21 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 字段标记提供者接口
|
||||
/// 所有 Tag 增强的 UI 组件需实现此接口,便于 Entry 在绑定阶段自动匹配
|
||||
/// </summary>
|
||||
public interface IXuiavrgFieldTagProvider
|
||||
{
|
||||
XuiavrgFieldTag EntryTag { get; }
|
||||
int TagIndex { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字段 Entry 接口
|
||||
/// 数据对象字段与 UI 组件之间的中间缓存层
|
||||
@@ -49,12 +38,15 @@ namespace XericUI.ReflectionUIGenerator
|
||||
/// <summary>生成的 UI 实例引用</summary>
|
||||
GameObject UIInstance { get; }
|
||||
|
||||
/// <summary>绑定的预制体组</summary>
|
||||
XuiavrgPropertyGroupScriptableObject BoundPrefabGroup { get; set; }
|
||||
/// <summary>绑定的预制体引用</summary>
|
||||
GameObject BoundPrefab { get; set; }
|
||||
|
||||
/// <summary>覆盖的预制体组名称</summary>
|
||||
string PrefabOverrideName { get; set; }
|
||||
|
||||
/// <summary>关联的 MVVM 绑定列表</summary>
|
||||
IReadOnlyList<AvrgMvvmBinding> Bindings { get; }
|
||||
|
||||
// === 生命周期 ===
|
||||
|
||||
/// <summary>Entry 创建时调用</summary>
|
||||
@@ -69,39 +61,22 @@ namespace XericUI.ReflectionUIGenerator
|
||||
/// <summary>销毁时调用</summary>
|
||||
void OnEntryDestroy();
|
||||
|
||||
// === 数据访问 ===
|
||||
// === 绑定管理 ===
|
||||
|
||||
/// <summary>获取指定成员的数据值</summary>
|
||||
object GetDataValue(string memberName);
|
||||
|
||||
/// <summary>设置指定成员的数据值</summary>
|
||||
void SetDataValue(string memberName, object value);
|
||||
|
||||
/// <summary>获取 Entry 下所有绑定成员的名称</summary>
|
||||
IEnumerable<string> GetMemberNames();
|
||||
/// <summary>添加 MVVM 绑定到此 Entry</summary>
|
||||
void AddBinding(AvrgMvvmBinding binding, AvrgMvvmScheduler scheduler);
|
||||
|
||||
// === UI 绑定 ===
|
||||
|
||||
/// <summary>
|
||||
/// 绑定 UI 组件
|
||||
/// </summary>
|
||||
/// <param name="uiInstance">UI 预制体实例</param>
|
||||
/// <param name="componentCache">
|
||||
/// 组件缓存列表。传入 null 时会自动收集 uiInstance 上所有 Component。
|
||||
/// 父 Entry 可将已收集的缓存传递给子 Entry 以提升性能
|
||||
/// </param>
|
||||
void BindUIComponent(GameObject uiInstance, List<Component> componentCache);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 字段临时 Entry 实现类
|
||||
/// 作为数据对象字段和 UI 组件之间的中间缓存层
|
||||
///
|
||||
/// 支持:
|
||||
/// - 多字段合并为一个 Entry(通过 XuiavrgFieldEntryGroupAttribute)
|
||||
/// - 嵌套 Entry(父 Entry 包含子 Entry)
|
||||
/// - 对象池复用(通过静态 ObjectPool)
|
||||
/// - MVVM 双向绑定的生命周期
|
||||
/// 作为数据对象和 UI 组件之间的中间缓存层
|
||||
/// </summary>
|
||||
public class XuiFieldTempEntry : IFieldEntry
|
||||
{
|
||||
@@ -130,73 +105,19 @@ namespace XericUI.ReflectionUIGenerator
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部绑定数据结构
|
||||
|
||||
/// <summary>
|
||||
/// 成员反射绑定信息
|
||||
/// </summary>
|
||||
internal class MemberBinding
|
||||
{
|
||||
/// <summary>绑定的数据目标对象</summary>
|
||||
public object valueTarget;
|
||||
|
||||
/// <summary>是否为属性(false 为字段)</summary>
|
||||
public bool isProperty;
|
||||
|
||||
/// <summary>属性信息</summary>
|
||||
public PropertyInfo propertyInfo;
|
||||
|
||||
/// <summary>字段信息</summary>
|
||||
public FieldInfo fieldInfo;
|
||||
|
||||
/// <summary>功能标记</summary>
|
||||
public XuiavrgFieldTag tag;
|
||||
|
||||
/// <summary>标记索引</summary>
|
||||
public int tagIndex;
|
||||
|
||||
/// <summary>
|
||||
/// 从绑定目标获取值
|
||||
/// </summary>
|
||||
public object GetValue()
|
||||
{
|
||||
if (valueTarget == null) return null;
|
||||
return isProperty
|
||||
? propertyInfo.GetValue(valueTarget)
|
||||
: fieldInfo.GetValue(valueTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值到绑定目标
|
||||
/// </summary>
|
||||
public void SetValue(object value)
|
||||
{
|
||||
if (valueTarget == null) return;
|
||||
if (isProperty)
|
||||
{
|
||||
if (propertyInfo.CanWrite)
|
||||
propertyInfo.SetValue(valueTarget, value);
|
||||
}
|
||||
else
|
||||
{
|
||||
fieldInfo.SetValue(valueTarget, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
private string _entryName;
|
||||
private string _entryPath;
|
||||
private IFieldEntry _parent;
|
||||
private List<XuiFieldTempEntry> _children = new List<XuiFieldTempEntry>();
|
||||
private Dictionary<string, MemberBinding> _bindings = new Dictionary<string, MemberBinding>();
|
||||
private List<AvrgMvvmBinding> _bindings = new List<AvrgMvvmBinding>();
|
||||
private AvrgMvvmScheduler _scheduler;
|
||||
private GameObject _uiInstance;
|
||||
private XuiavrgPropertyGroupScriptableObject _boundPrefabGroup;
|
||||
private GameObject _boundPrefab;
|
||||
private string _prefabOverrideName;
|
||||
private int _fieldOrder;
|
||||
private bool _uiBound; // 防止重复绑定
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -237,10 +158,10 @@ namespace XericUI.ReflectionUIGenerator
|
||||
|
||||
public GameObject UIInstance => _uiInstance;
|
||||
|
||||
public XuiavrgPropertyGroupScriptableObject BoundPrefabGroup
|
||||
public GameObject BoundPrefab
|
||||
{
|
||||
get => _boundPrefabGroup;
|
||||
set => _boundPrefabGroup = value;
|
||||
get => _boundPrefab;
|
||||
set => _boundPrefab = value;
|
||||
}
|
||||
|
||||
public string PrefabOverrideName
|
||||
@@ -249,6 +170,8 @@ namespace XericUI.ReflectionUIGenerator
|
||||
set => _prefabOverrideName = value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<AvrgMvvmBinding> Bindings => _bindings;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 初始化方法
|
||||
@@ -263,6 +186,7 @@ namespace XericUI.ReflectionUIGenerator
|
||||
Tag = tag;
|
||||
TagIndex = tagIndex;
|
||||
_fieldOrder = 0;
|
||||
_uiBound = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -271,7 +195,7 @@ namespace XericUI.ReflectionUIGenerator
|
||||
public void SetParent(IFieldEntry parent)
|
||||
{
|
||||
_parent = parent;
|
||||
_entryPath = null; // 重置路径,下次访问重新计算
|
||||
_entryPath = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -285,98 +209,19 @@ namespace XericUI.ReflectionUIGenerator
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 注册成员绑定
|
||||
/// 添加 MVVM 绑定到此 Entry
|
||||
/// </summary>
|
||||
/// <param name="memberName">成员名称</param>
|
||||
/// <param name="memberInfo">反射信息</param>
|
||||
/// <param name="target">数据目标对象</param>
|
||||
/// <param name="tag">功能标记</param>
|
||||
/// <param name="tagIndex">标记索引</param>
|
||||
public void RegisterBinding(string memberName, MemberInfo memberInfo, object target,
|
||||
XuiavrgFieldTag tag = XuiavrgFieldTag.None, int tagIndex = 0)
|
||||
public void AddBinding(AvrgMvvmBinding binding, AvrgMvvmScheduler scheduler)
|
||||
{
|
||||
MemberBinding binding = new MemberBinding
|
||||
{
|
||||
valueTarget = target,
|
||||
tag = tag,
|
||||
tagIndex = tagIndex
|
||||
};
|
||||
if (binding == null) return;
|
||||
_bindings.Add(binding);
|
||||
_scheduler = scheduler;
|
||||
|
||||
switch (memberInfo)
|
||||
if (Tag == XuiavrgFieldTag.None && binding.MetaData?.Tag != XuiavrgFieldTag.None)
|
||||
{
|
||||
case PropertyInfo propInfo:
|
||||
binding.isProperty = true;
|
||||
binding.propertyInfo = propInfo;
|
||||
break;
|
||||
case FieldInfo fieldInfo:
|
||||
binding.isProperty = false;
|
||||
binding.fieldInfo = fieldInfo;
|
||||
break;
|
||||
Tag = binding.MetaData.Tag;
|
||||
TagIndex = binding.MetaData.TagIndex;
|
||||
}
|
||||
|
||||
_bindings[memberName] = binding;
|
||||
|
||||
// 如果是第一个注册的绑定,设置主 Tag
|
||||
if (Tag == XuiavrgFieldTag.None && tag != XuiavrgFieldTag.None)
|
||||
{
|
||||
Tag = tag;
|
||||
TagIndex = tagIndex;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 数据访问
|
||||
|
||||
public object GetDataValue(string memberName)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
return binding.GetValue();
|
||||
|
||||
// 查找子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
var value = child.GetDataValue(memberName);
|
||||
if (value != null) return value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetDataValue(string memberName, object value)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
{
|
||||
binding.SetValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查找子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
if (child.HasBinding(memberName))
|
||||
{
|
||||
child.SetDataValue(memberName, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetMemberNames() => _bindings.Keys;
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否包含指定成员绑定
|
||||
/// </summary>
|
||||
public bool HasBinding(string memberName) => _bindings.ContainsKey(memberName);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定成员的标记
|
||||
/// </summary>
|
||||
public XuiavrgFieldTag GetMemberTag(string memberName)
|
||||
{
|
||||
if (_bindings.TryGetValue(memberName, out var binding))
|
||||
return binding.tag;
|
||||
return XuiavrgFieldTag.None;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -386,7 +231,7 @@ namespace XericUI.ReflectionUIGenerator
|
||||
public void BindUIComponent(GameObject uiInstance, List<Component> componentCache)
|
||||
{
|
||||
_uiInstance = uiInstance;
|
||||
if (_uiInstance == null) return;
|
||||
if (_uiInstance == null || _uiBound) return;
|
||||
|
||||
if (componentCache == null)
|
||||
{
|
||||
@@ -394,183 +239,131 @@ namespace XericUI.ReflectionUIGenerator
|
||||
_uiInstance.GetComponentsInChildren(true, componentCache);
|
||||
}
|
||||
|
||||
BindUIComponentWithCache(_uiInstance, componentCache);
|
||||
// 遍历组件缓存,寻找 XUIAvrgMDValue 按 Tag 匹配
|
||||
foreach (var comp in componentCache)
|
||||
{
|
||||
if (comp is XUIAvrgMDValue mdValue)
|
||||
{
|
||||
if (mdValue.EntryTag == this.Tag &&
|
||||
(this.TagIndex == 0 || mdValue.TagIndex == this.TagIndex))
|
||||
{
|
||||
BindComponentWithBinding(mdValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 子 Entry 复用组件缓存
|
||||
foreach (var child in _children)
|
||||
{
|
||||
child.BindUIComponent(_uiInstance, componentCache);
|
||||
}
|
||||
}
|
||||
|
||||
private void BindUIComponentWithCache(GameObject uiInstance, List<Component> componentCache)
|
||||
{
|
||||
// 遍历组件缓存,匹配 tag
|
||||
var locators = new List<XuiavrgLocator>();
|
||||
foreach (var comp in componentCache)
|
||||
{
|
||||
if (comp is XuiavrgLocator locator)
|
||||
{
|
||||
locators.Add(locator);
|
||||
}
|
||||
|
||||
if (comp is IXuiavrgFieldTagProvider tagProvider)
|
||||
{
|
||||
// 匹配 Tag 和 TagIndex
|
||||
if (tagProvider.EntryTag == this.Tag &&
|
||||
(this.TagIndex == 0 || tagProvider.TagIndex == this.TagIndex))
|
||||
{
|
||||
// 找到匹配的 UI 组件,建立绑定
|
||||
BuildComponentBinding(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有 IXuiavrgFieldTagProvider,尝试通过 Locator 匹配
|
||||
if (locators.Count > 0)
|
||||
{
|
||||
BuildLocatorBindings(locators);
|
||||
}
|
||||
_uiBound = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过标签提供者建立组件绑定
|
||||
/// 将 XUIAvrgMDValue 与 MVVM 绑定连接
|
||||
/// </summary>
|
||||
private void BuildComponentBinding(Component comp)
|
||||
private void BindComponentWithBinding(XUIAvrgMDValue mdValue)
|
||||
{
|
||||
// 根据组件类型和标记类型进行数据同步
|
||||
switch (comp)
|
||||
{
|
||||
case UnityEngine.UI.Text uiText when Tag == XuiavrgFieldTag.Label || Tag == XuiavrgFieldTag.Output:
|
||||
uiText.text = GetFirstBindingValue()?.ToString() ?? "";
|
||||
break;
|
||||
case UnityEngine.UI.Button _ when Tag == XuiavrgFieldTag.Button:
|
||||
// 按钮绑定在 BindUIEvents 中处理
|
||||
break;
|
||||
case UnityEngine.UI.Toggle uiToggle when Tag == XuiavrgFieldTag.Toggle:
|
||||
if (GetFirstBindingValue() is bool boolVal)
|
||||
uiToggle.isOn = boolVal;
|
||||
break;
|
||||
case UnityEngine.UI.Slider uiSlider when Tag == XuiavrgFieldTag.Slider:
|
||||
if (GetFirstBindingValue() is float floatVal)
|
||||
uiSlider.value = floatVal;
|
||||
break;
|
||||
case UnityEngine.UI.InputField uiInput when Tag == XuiavrgFieldTag.Input:
|
||||
uiInput.text = GetFirstBindingValue()?.ToString() ?? "";
|
||||
break;
|
||||
case UnityEngine.UI.Dropdown uiDropdown when Tag == XuiavrgFieldTag.Dropdown:
|
||||
if (GetFirstBindingValue() is int intVal)
|
||||
uiDropdown.value = intVal;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (_bindings.Count == 0) return;
|
||||
|
||||
/// <summary>
|
||||
/// 通过 Locator 建立绑定
|
||||
/// </summary>
|
||||
private void BuildLocatorBindings(List<XuiavrgLocator> locators)
|
||||
{
|
||||
foreach (var locator in locators)
|
||||
var primaryBinding = _bindings[0];
|
||||
|
||||
// 数据 → UI:订阅数据变更
|
||||
primaryBinding.OnUIDataChanged += value => UpdateUIComponent(mdValue, value);
|
||||
|
||||
// UI → 数据:订阅 UI 交互事件
|
||||
// 根据组件类型决定 UI 事件
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.Button uiButton))
|
||||
{
|
||||
foreach (var locProp in locator.Properties)
|
||||
uiButton.onClick.AddListener(() =>
|
||||
{
|
||||
// 按 propertyName 匹配成员名
|
||||
if (_bindings.TryGetValue(locProp.propertyName, out var binding))
|
||||
{
|
||||
// 从数据端同步到 UI
|
||||
var dataValue = binding.GetValue();
|
||||
locProp.SetValue(dataValue);
|
||||
}
|
||||
}
|
||||
primaryBinding.WriteFromUI(true);
|
||||
_scheduler?.MarkUIDirty(primaryBinding);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取第一个绑定的值
|
||||
/// </summary>
|
||||
private object GetFirstBindingValue()
|
||||
{
|
||||
foreach (var kvp in _bindings)
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.Toggle uiToggle))
|
||||
{
|
||||
var val = kvp.Value.GetValue();
|
||||
if (val != null) return val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 事件绑定
|
||||
|
||||
/// <summary>
|
||||
/// 绑定 UI 交互事件(双向绑定)
|
||||
/// </summary>
|
||||
public void BindUIEvents()
|
||||
{
|
||||
if (_uiInstance == null) return;
|
||||
|
||||
var allComponents = new List<Component>();
|
||||
_uiInstance.GetComponentsInChildren(true, allComponents);
|
||||
|
||||
foreach (var comp in allComponents)
|
||||
{
|
||||
if (comp is IXuiavrgFieldTagProvider tagProvider)
|
||||
uiToggle.onValueChanged.AddListener(val =>
|
||||
{
|
||||
if (tagProvider.EntryTag != this.Tag) continue;
|
||||
BindComponentEvents(comp);
|
||||
}
|
||||
primaryBinding.WriteFromUI(val);
|
||||
_scheduler?.MarkUIDirty(primaryBinding);
|
||||
});
|
||||
}
|
||||
|
||||
// 子 Entry 事件绑定
|
||||
foreach (var child in _children)
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.Slider uiSlider))
|
||||
{
|
||||
child.BindUIEvents();
|
||||
uiSlider.onValueChanged.AddListener(val =>
|
||||
{
|
||||
primaryBinding.WriteFromUI(val);
|
||||
_scheduler?.MarkUIDirty(primaryBinding);
|
||||
});
|
||||
}
|
||||
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.InputField uiInput))
|
||||
{
|
||||
uiInput.onEndEdit.AddListener(val =>
|
||||
{
|
||||
primaryBinding.WriteFromUI(val);
|
||||
_scheduler?.MarkUIDirty(primaryBinding);
|
||||
});
|
||||
}
|
||||
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.Dropdown uiDropdown))
|
||||
{
|
||||
uiDropdown.onValueChanged.AddListener(val =>
|
||||
{
|
||||
primaryBinding.WriteFromUI(val);
|
||||
_scheduler?.MarkUIDirty(primaryBinding);
|
||||
});
|
||||
}
|
||||
|
||||
// 广播元数据到 XUIAvrgMDValue(触发 IXUIAvrgMDValueStyle 子组件)
|
||||
mdValue.InitializeWithMetaData(primaryBinding.MetaData);
|
||||
}
|
||||
|
||||
private void BindComponentEvents(Component comp)
|
||||
/// <summary>
|
||||
/// 更新 UI 组件的显示值(通过 XUIAvrgMDValue 所在 GameObject 上的具体组件)
|
||||
/// </summary>
|
||||
private static void UpdateUIComponent(XUIAvrgMDValue mdValue, object value)
|
||||
{
|
||||
switch (comp)
|
||||
{
|
||||
case UnityEngine.UI.Button uiButton:
|
||||
uiButton.onClick.AddListener(() =>
|
||||
{
|
||||
SetFirstBindingValue(true);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Toggle uiToggle:
|
||||
uiToggle.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Slider uiSlider:
|
||||
uiSlider.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.InputField uiInput:
|
||||
uiInput.onEndEdit.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
case UnityEngine.UI.Dropdown uiDropdown:
|
||||
uiDropdown.onValueChanged.AddListener(val =>
|
||||
{
|
||||
SetFirstBindingValue(val);
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (mdValue == null || value == null) return;
|
||||
|
||||
private void SetFirstBindingValue(object value)
|
||||
{
|
||||
foreach (var kvp in _bindings)
|
||||
// Text
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.Text uiText))
|
||||
{
|
||||
kvp.Value.SetValue(value);
|
||||
break;
|
||||
uiText.text = value.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle
|
||||
if (value is bool boolVal && mdValue.TryGetComponent(out UnityEngine.UI.Toggle uiToggle))
|
||||
{
|
||||
uiToggle.isOn = boolVal;
|
||||
return;
|
||||
}
|
||||
|
||||
// Slider
|
||||
if (value is float floatVal && mdValue.TryGetComponent(out UnityEngine.UI.Slider uiSlider))
|
||||
{
|
||||
uiSlider.value = floatVal;
|
||||
return;
|
||||
}
|
||||
|
||||
// InputField
|
||||
if (mdValue.TryGetComponent(out UnityEngine.UI.InputField uiInput))
|
||||
{
|
||||
uiInput.text = value.ToString();
|
||||
return;
|
||||
}
|
||||
|
||||
// Dropdown
|
||||
if (value is int intVal && mdValue.TryGetComponent(out UnityEngine.UI.Dropdown uiDropdown))
|
||||
{
|
||||
uiDropdown.value = intVal;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,11 +378,16 @@ namespace XericUI.ReflectionUIGenerator
|
||||
_parent = null;
|
||||
_fieldOrder = 0;
|
||||
_prefabOverrideName = null;
|
||||
_uiBound = false;
|
||||
}
|
||||
|
||||
public void OnEntryBind()
|
||||
{
|
||||
BindUIEvents();
|
||||
foreach (var binding in _bindings)
|
||||
{
|
||||
var value = binding.ReadFromDataSource();
|
||||
binding.NotifyUIUpdate(value);
|
||||
}
|
||||
}
|
||||
|
||||
public void OnEntryRelease()
|
||||
@@ -598,16 +396,20 @@ namespace XericUI.ReflectionUIGenerator
|
||||
_entryPath = null;
|
||||
_parent = null;
|
||||
_uiInstance = null;
|
||||
_boundPrefabGroup = null;
|
||||
_boundPrefab = null;
|
||||
_prefabOverrideName = null;
|
||||
Tag = XuiavrgFieldTag.None;
|
||||
TagIndex = 0;
|
||||
_fieldOrder = 0;
|
||||
_uiBound = false;
|
||||
|
||||
// 清理绑定引用
|
||||
foreach (var binding in _bindings)
|
||||
{
|
||||
binding.ClearUIEvent();
|
||||
}
|
||||
_bindings.Clear();
|
||||
_scheduler = null;
|
||||
|
||||
// 递归释放子 Entry
|
||||
foreach (var child in _children)
|
||||
{
|
||||
Release(child);
|
||||
@@ -617,9 +419,14 @@ namespace XericUI.ReflectionUIGenerator
|
||||
|
||||
public void OnEntryDestroy()
|
||||
{
|
||||
foreach (var binding in _bindings)
|
||||
{
|
||||
binding.ClearUIEvent();
|
||||
}
|
||||
_bindings.Clear();
|
||||
_children.Clear();
|
||||
_uiInstance = null;
|
||||
_scheduler = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
-107
@@ -1,107 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using XericLibrary.Runtime.MacroLibrary;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// [已弃用] 属性映射器。请使用 XuiavrgPropertyGroupScriptableObject 替代。
|
||||
/// 功能已整合到 PropertyGroup 中,此类保留仅用于向后兼容。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[Obsolete("Use XuiavrgPropertyGroupScriptableObject instead. Property mapping is now integrated directly into the group.")]
|
||||
public class XuiavrPropertyScriptableObject : ScriptableObject
|
||||
{
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("属性类型")]
|
||||
[ValueDropdown("AssemblyValueTypeNames")]
|
||||
#endif
|
||||
public string valueType;
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("属性名称")]
|
||||
#endif
|
||||
public string propertyName;
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("界面预制体")]
|
||||
#endif
|
||||
public GameObject prefab;
|
||||
|
||||
/// <summary>
|
||||
/// 属性类型
|
||||
/// </summary>
|
||||
public System.Type ValueType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_valueType != null)
|
||||
return _valueType;
|
||||
return _valueType = Type.GetType(valueType);
|
||||
}
|
||||
}
|
||||
private System.Type _valueType;
|
||||
|
||||
#if ODIN_INSPECTOR // 属性类型名称枚举
|
||||
private IEnumerable AssemblyValueTypeNames
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_assemblyValueTypeName != null)
|
||||
return _assemblyValueTypeName;
|
||||
_assemblyValueTypeName = new ValueDropdownList<string>();
|
||||
|
||||
// foreach (var typeName in MacroReflection.GetAllAssignableTypeInAppDomainAssembly(
|
||||
// typeof(float),
|
||||
// typeof(string),
|
||||
// typeof(bool),
|
||||
// typeof(Enum),
|
||||
// typeof(int)
|
||||
// ).Select(a => a.FullName))
|
||||
// {
|
||||
// _assemblyValueTypeName.Add(typeName);
|
||||
// }
|
||||
|
||||
_assemblyValueTypeName.AddRange(AllValueType.Select(a => new ValueDropdownItem<string>(a.Name, a.FullName)));
|
||||
return _assemblyValueTypeName;
|
||||
}
|
||||
}
|
||||
private static ValueDropdownList<string> _assemblyValueTypeName = null;
|
||||
#endif
|
||||
|
||||
/// <summary>
|
||||
/// 直接定义所有需要检查的类型
|
||||
/// </summary>
|
||||
public static IEnumerable<Type> AllValueType
|
||||
{
|
||||
get
|
||||
{
|
||||
yield return typeof(Enum);
|
||||
yield return typeof(decimal);
|
||||
yield return typeof(double);
|
||||
yield return typeof(float);
|
||||
yield return typeof(ulong);
|
||||
yield return typeof(long);
|
||||
yield return typeof(int);
|
||||
yield return typeof(uint);
|
||||
yield return typeof(short);
|
||||
yield return typeof(ushort);
|
||||
yield return typeof(byte);
|
||||
yield return typeof(sbyte);
|
||||
yield return typeof(string);
|
||||
yield return typeof(char);
|
||||
yield return typeof(bool);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be0acbdac8e549578dcccf8c076a5433
|
||||
timeCreated: 1776998687
|
||||
@@ -1,133 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试用枚举 - 用于下拉选择测试
|
||||
/// </summary>
|
||||
public enum XuiavrgTestQuality
|
||||
{
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
Epic,
|
||||
Legendary
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反射界面生成器测试数据类
|
||||
/// 涵盖新版 Entry 体系的所有特性:编组、排序、预制体覆盖、多种字段类型
|
||||
///
|
||||
/// 使用方式:
|
||||
/// 1. 在 Hierarchy 右键 -> Xeric UI -> Create Reflection UI Example 创建示例节点
|
||||
/// 2. 创建 XuiavrgPropertyGroupScriptableObject 并设置 groupName 和 supportedTags
|
||||
/// 3. 将编组对应的预制体拖入
|
||||
/// 4. 调用 manager.BuildEntries(new XuiavrgExampleTestData()) 构建 Entry
|
||||
/// 5. 调用 manager.GenerateUI() 生成界面
|
||||
/// 或直接使用兼容方法: manager.SchemaHydration(new XuiavrgExampleTestData())
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class XuiavrgExampleTestData
|
||||
{
|
||||
// ==================== 基础数值类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(1)]
|
||||
[XuiavrgNumberCheck(0, 100)]
|
||||
public int intValue = 42;
|
||||
|
||||
[XuiavrgFieldOrder(2)]
|
||||
[XuiavrgNumberCheck(0f, 999f)]
|
||||
public float floatValue = 3.14f;
|
||||
|
||||
[XuiavrgFieldOrder(3)]
|
||||
public double doubleValue = 123.456;
|
||||
|
||||
[XuiavrgFieldOrder(4)]
|
||||
[XuiavrgNumberCheck(0, 999999)]
|
||||
public long longValue = 100000;
|
||||
|
||||
// ==================== 多字段编组示例 ====================
|
||||
// 以下三个字段合并为一个 Entry,共享一个 UI 组件实例
|
||||
// UI 组件上需要有 Label、Context、Unit 对应的 IXuiavrgFieldTagProvider
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Label)]
|
||||
[XuiavrgFieldOrder(5)]
|
||||
public string speedLabel = "速度";
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Context)]
|
||||
[XuiavrgNumberCheck(-999f, 999f)]
|
||||
public float speedValue = 0.1f;
|
||||
|
||||
[XuiavrgFieldEntryGroup("speed", XuiavrgFieldTag.Unit)]
|
||||
public string speedUnit = "m/s";
|
||||
|
||||
// 第二个编组示例 - 健康值
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Label)]
|
||||
[XuiavrgFieldOrder(6)]
|
||||
public string healthLabel = "生命值";
|
||||
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Context)]
|
||||
[XuiavrgNumberCheck(0, 9999)]
|
||||
public float healthValue = 100f;
|
||||
|
||||
[XuiavrgFieldEntryGroup("health", XuiavrgFieldTag.Unit)]
|
||||
public string healthUnit = "HP";
|
||||
|
||||
// ==================== 字符串类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(7)]
|
||||
[XuiavrgRequired]
|
||||
[XuiavrgStringCheck(minLength: 2, maxLength: 16)]
|
||||
public string playerName = "DefaultName";
|
||||
|
||||
[XuiavrgFieldOrder(8)]
|
||||
[XuiavrgNotEmpty]
|
||||
[XuiavrgStringCheck(onlyEmail: false)]
|
||||
public string emailAddress = "example@mail.com";
|
||||
|
||||
[XuiavrgFieldOrder(9)]
|
||||
[XuiavrgFieldPrefabOverride("MultilineTextArea")]
|
||||
[XuiavrgStringCheck(maxLength: 500)]
|
||||
public string biography = "这是一段个人简介...";
|
||||
|
||||
// ==================== 布尔类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(10)]
|
||||
public bool enableFeature = true;
|
||||
|
||||
[XuiavrgFieldOrder(11)]
|
||||
public bool advancedMode = false;
|
||||
|
||||
// ==================== 枚举类型 ====================
|
||||
|
||||
[XuiavrgFieldOrder(12)]
|
||||
public XuiavrgTestQuality quality = XuiavrgTestQuality.High;
|
||||
|
||||
// ==================== 隐藏属性 ====================
|
||||
|
||||
[XuiavrgHide(true)]
|
||||
public string hiddenField = "此字段被隐藏,不会生成 Entry";
|
||||
|
||||
// ==================== 不同尺寸 ====================
|
||||
|
||||
[XuiavrgFieldOrder(13)]
|
||||
[XuiavrgAttribute("小号", "size_small", null, size: XuiavrgSize.Small)]
|
||||
public string smallSizeField = "小尺寸";
|
||||
|
||||
[XuiavrgFieldOrder(14)]
|
||||
[XuiavrgAttribute("大号", "size_large", null, size: XuiavrgSize.Large)]
|
||||
public string largeSizeField = "大尺寸";
|
||||
|
||||
// ==================== 集合类型(兼容旧标记) ====================
|
||||
|
||||
[XuiavrgFieldOrder(15)]
|
||||
[XuiavrgCollectionCheck(1, 10, true)]
|
||||
public List<string> tags = new List<string> { "Unity", "C#", "UI" };
|
||||
|
||||
// ==================== 按钮触发器 ====================
|
||||
|
||||
[XuiavrgFieldOrder(16)]
|
||||
public bool submitTrigger = false;
|
||||
}
|
||||
}
|
||||
+206
-259
@@ -1,7 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Pool;
|
||||
using Object = UnityEngine.Object;
|
||||
@@ -9,41 +7,22 @@ using Object = UnityEngine.Object;
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 节点下生成反射属性管理器
|
||||
/// 节点下生成反射属性管理器 — Metadata-Driven View Model (MDVM 元数据驱动的视图数据模型)
|
||||
/// 负责:构建 Entry → 创建组容器 → 生成 UI → 绑定数据
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 管理器需要挂载在 Unity 节点中,管理器中指定的属性承载节点将用于生成属性内容。
|
||||
///
|
||||
/// 新的生命周期流程:
|
||||
/// 1. BuildEntries(object payload) — 传入数据对象,构建 Entry 列表
|
||||
/// 2. OnPostBuildEntries(entries) — 虚方法,可重写添加额外 Entry
|
||||
/// 3. 自动按 FieldOrder 排序
|
||||
/// 4. GenerateUI() — 根据 Entry 列表生成 UI 实例并绑定
|
||||
///
|
||||
/// 对象池说明:
|
||||
/// - Entry 通过 XuiFieldTempEntry.Pool 静态池管理
|
||||
/// - UI 预制体通过内部 PropertyGroupPools 字典管理,以 PropertyGroupScriptableObject 为键
|
||||
/// </remarks>
|
||||
public class XuiavrgInspectorManager : MonoBehaviour
|
||||
{
|
||||
#region 对象池
|
||||
|
||||
/// <summary>
|
||||
/// UI 预制体对象池,以 PropertyGroupScriptableObject 为键
|
||||
/// </summary>
|
||||
private Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>> _propertyGroupPools
|
||||
= new Dictionary<XuiavrgPropertyGroupScriptableObject, ObjectPool<GameObject>>();
|
||||
private Dictionary<GameObject, ObjectPool<GameObject>> _prefabPools
|
||||
= new Dictionary<GameObject, ObjectPool<GameObject>>();
|
||||
|
||||
/// <summary>
|
||||
/// 从指定 PropertyGroup 的预制体池获取 UI 实例
|
||||
/// </summary>
|
||||
public GameObject PropertyGroupPoolGet(XuiavrgPropertyGroupScriptableObject group)
|
||||
public GameObject PrefabPoolGet(GameObject prefab)
|
||||
{
|
||||
if (group == null) return null;
|
||||
if (prefab == null) return null;
|
||||
|
||||
if (!_propertyGroupPools.TryGetValue(group, out var pool))
|
||||
if (!_prefabPools.TryGetValue(prefab, out var pool))
|
||||
{
|
||||
var prefab = group.prefab;
|
||||
pool = new ObjectPool<GameObject>(
|
||||
createFunc: () =>
|
||||
prefab != null
|
||||
@@ -59,19 +38,16 @@ namespace XericUI.ReflectionUIGenerator
|
||||
defaultCapacity: 4,
|
||||
maxSize: 64
|
||||
);
|
||||
_propertyGroupPools[group] = pool;
|
||||
_prefabPools[prefab] = pool;
|
||||
}
|
||||
|
||||
return pool.Get();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 UI 实例归还到指定 PropertyGroup 的预制体池
|
||||
/// </summary>
|
||||
public void PropertyGroupPoolRelease(XuiavrgPropertyGroupScriptableObject group, GameObject instance)
|
||||
public void PrefabPoolRelease(GameObject prefab, GameObject instance)
|
||||
{
|
||||
if (group == null || instance == null) return;
|
||||
if (_propertyGroupPools.TryGetValue(group, out var pool))
|
||||
if (prefab == null || instance == null) return;
|
||||
if (_prefabPools.TryGetValue(prefab, out var pool))
|
||||
{
|
||||
pool.Release(instance);
|
||||
}
|
||||
@@ -81,219 +57,109 @@ namespace XericUI.ReflectionUIGenerator
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空所有预制体对象池
|
||||
/// </summary>
|
||||
public void ClearAllPropertyGroupPools()
|
||||
public void ClearAllPrefabPools()
|
||||
{
|
||||
foreach (var pool in _propertyGroupPools.Values)
|
||||
foreach (var pool in _prefabPools.Values)
|
||||
{
|
||||
pool.Clear();
|
||||
}
|
||||
_propertyGroupPools.Clear();
|
||||
_prefabPools.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 字段
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("当前使用的 UI 主题名称。切换主题会立即销毁所有已生成 UI 和对象池,需重新 BuildEntries + GenerateUI")]
|
||||
private string _currentTheme = XuiavrgPropertyGroupScriptableObject.DefaultThemeName;
|
||||
|
||||
/// <summary>
|
||||
/// 当前 UI 主题名称
|
||||
/// </summary>
|
||||
public string CurrentTheme => _currentTheme;
|
||||
|
||||
/// <summary>
|
||||
/// 当前活跃的 Entry 列表
|
||||
/// </summary>
|
||||
public List<IFieldEntry> CurrentEntries { get; private set; } = new List<IFieldEntry>();
|
||||
public AvrgMvvmBindingCollection CurrentBindingCollection { get; private set; }
|
||||
public AvrgMemberMetaDataSet CurrentMetaDataSet { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前传入的数据负载
|
||||
/// 当前活跃的组容器字典
|
||||
/// </summary>
|
||||
public object CurrentPayload { get; private set; }
|
||||
private Dictionary<string, XuiAvrgMDGroup> _activeGroups = new Dictionary<string, XuiAvrgMDGroup>();
|
||||
|
||||
/// <summary>
|
||||
/// 切换 UI 主题。会立即销毁所有已生成 UI 和对象池。
|
||||
/// 调用后需重新 BuildEntries + GenerateUI。
|
||||
/// </summary>
|
||||
/// <param name="themeName">新的主题名称</param>
|
||||
public void SetTheme(string themeName)
|
||||
{
|
||||
if (_currentTheme == themeName) return;
|
||||
|
||||
_currentTheme = string.IsNullOrEmpty(themeName)
|
||||
? XuiavrgPropertyGroupScriptableObject.DefaultThemeName
|
||||
: themeName;
|
||||
|
||||
// 销毁所有已生成 UI 和对象池
|
||||
ClearAll();
|
||||
}
|
||||
[Tooltip("类型-预制体映射配置 ScriptableObject")]
|
||||
public AvrgTypePrefabMappingConfig typePrefabMappingConfig;
|
||||
|
||||
#endregion
|
||||
|
||||
#region 核心生命周期
|
||||
|
||||
/// <summary>
|
||||
/// 传入对象实例,构建 Entry 列表
|
||||
/// </summary>
|
||||
/// <param name="payload">数据端对象</param>
|
||||
public void BuildEntries(object payload)
|
||||
public void BuildFromMetaDataSet(AvrgMemberMetaDataSet metaDataSet,
|
||||
AvrgMvvmBindingCollection bindingCollection = null)
|
||||
{
|
||||
// 清空之前的 Entry
|
||||
ClearCurrentEntries();
|
||||
CurrentPayload = payload;
|
||||
CurrentMetaDataSet = metaDataSet;
|
||||
CurrentBindingCollection = bindingCollection;
|
||||
|
||||
if (payload == null)
|
||||
if (metaDataSet == null)
|
||||
{
|
||||
Debug.LogError("[Xuiavrg] 未指定有效负载,无法构建 Entry");
|
||||
Debug.LogError("[Xuiavrg] 未指定元数据集,无法构建 Entry");
|
||||
return;
|
||||
}
|
||||
|
||||
var type = payload.GetType();
|
||||
var memberInfos = type.GetRuntimeMemberInfo().ToList();
|
||||
|
||||
// 用于合并编组 Entry 的临时字典
|
||||
var groupEntries = new Dictionary<string, XuiFieldTempEntry>();
|
||||
|
||||
foreach (var memberInfo in memberInfos)
|
||||
foreach (var metaData in metaDataSet.MetaDataList)
|
||||
{
|
||||
// 检查隐藏标记
|
||||
var hideAttr = memberInfo.GetCustomAttribute<XuiavrgHideAttribute>();
|
||||
if (hideAttr != null && hideAttr.Hide)
|
||||
if (metaData.Hide)
|
||||
continue;
|
||||
|
||||
// 检查编组标记
|
||||
var groupAttr = memberInfo.GetCustomAttribute<XuiavrgFieldEntryGroupAttribute>();
|
||||
var entry = XuiFieldTempEntry.Get();
|
||||
entry.Initialize(
|
||||
string.IsNullOrEmpty(metaData.GroupPath) ? metaData.MemberName : metaData.GroupPath,
|
||||
metaData.Tag,
|
||||
metaData.TagIndex);
|
||||
|
||||
if (groupAttr != null)
|
||||
if (!string.IsNullOrEmpty(metaData.PrefabOverrideName))
|
||||
entry.PrefabOverrideName = metaData.PrefabOverrideName;
|
||||
|
||||
if (bindingCollection != null)
|
||||
{
|
||||
// 编组字段:合并到同名 Entry
|
||||
if (!groupEntries.TryGetValue(groupAttr.GroupPath, out var groupEntry))
|
||||
var matchedBindings = bindingCollection.FindByMemberName(metaData.MemberName);
|
||||
if (matchedBindings != null)
|
||||
{
|
||||
groupEntry = XuiFieldTempEntry.Get();
|
||||
groupEntry.Initialize(groupAttr.GroupPath);
|
||||
groupEntries[groupAttr.GroupPath] = groupEntry;
|
||||
CurrentEntries.Add(groupEntry);
|
||||
}
|
||||
|
||||
groupEntry.RegisterBinding(
|
||||
memberInfo.Name, memberInfo, payload,
|
||||
groupAttr.Tag, groupAttr.TagIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通字段:创建独立 Entry
|
||||
var entry = XuiFieldTempEntry.Get();
|
||||
entry.Initialize(memberInfo.Name);
|
||||
entry.RegisterBinding(memberInfo.Name, memberInfo, payload);
|
||||
|
||||
// 检查预制体覆盖
|
||||
var overrideAttr = memberInfo.GetCustomAttribute<XuiavrgFieldPrefabOverrideAttribute>();
|
||||
if (overrideAttr != null)
|
||||
{
|
||||
entry.PrefabOverrideName = overrideAttr.PrefabGroupName;
|
||||
}
|
||||
|
||||
CurrentEntries.Add(entry);
|
||||
}
|
||||
|
||||
// 应用排序标记
|
||||
var orderAttr = memberInfo.GetCustomAttribute<XuiavrgFieldOrderAttribute>();
|
||||
if (orderAttr != null)
|
||||
{
|
||||
// 找到该成员对应的 Entry(可能是编组 Entry)
|
||||
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
|
||||
{
|
||||
if (gEntry.FieldOrder == 0 || orderAttr.Order < gEntry.FieldOrder)
|
||||
gEntry.FieldOrder = orderAttr.Order;
|
||||
entry.AddBinding(matchedBindings, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 普通字段的排序应用到其 Entry
|
||||
var normalEntry = CurrentEntries.LastOrDefault();
|
||||
if (normalEntry != null)
|
||||
normalEntry.FieldOrder = orderAttr.Order;
|
||||
foreach (var binding in bindingCollection)
|
||||
{
|
||||
entry.AddBinding(binding, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用旧的 IXuiavrgAttribute(兼容)
|
||||
ApplyLegacyAttributes(memberInfo, payload, groupAttr, groupEntries);
|
||||
entry.FieldOrder = metaData.FieldOrder;
|
||||
CurrentEntries.Add(entry);
|
||||
}
|
||||
|
||||
// 调用后期回调
|
||||
OnPostBuildEntries(CurrentEntries);
|
||||
|
||||
// 排序
|
||||
SortEntries();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用旧的 IXuiavrgAttribute 体系中的属性(兼容保留)
|
||||
/// </summary>
|
||||
private void ApplyLegacyAttributes(
|
||||
MemberInfo memberInfo, object payload,
|
||||
XuiavrgFieldEntryGroupAttribute groupAttr,
|
||||
Dictionary<string, XuiFieldTempEntry> groupEntries)
|
||||
public void SetBindingCollection(AvrgMvvmBindingCollection bindingCollection)
|
||||
{
|
||||
// 处理旧的 XuiavrgAttribute(设置 labelName, inspectorOrder 等)
|
||||
var oldAttr = memberInfo.GetCustomAttribute<XuiavrgAttribute>();
|
||||
if (oldAttr != null)
|
||||
CurrentBindingCollection = bindingCollection;
|
||||
|
||||
if (CurrentEntries != null && bindingCollection != null)
|
||||
{
|
||||
// 创建临时的 InspectorProperty 来接收绑定
|
||||
var tempProp = new XuiavrgInspectorProperty();
|
||||
tempProp.RegsiterDataValue(memberInfo, payload);
|
||||
oldAttr.BindProperty(tempProp);
|
||||
|
||||
// 将相关属性同步到 Entry
|
||||
IFieldEntry targetEntry;
|
||||
if (groupAttr != null && groupEntries.TryGetValue(groupAttr.GroupPath, out var gEntry))
|
||||
targetEntry = gEntry;
|
||||
else
|
||||
targetEntry = CurrentEntries.LastOrDefault(e => !e.IsGroupEntry || e == null) ?? CurrentEntries.LastOrDefault();
|
||||
|
||||
if (targetEntry != null)
|
||||
foreach (var entry in CurrentEntries)
|
||||
{
|
||||
if (tempProp.inspectorOrder != 0 && targetEntry.FieldOrder == 0)
|
||||
targetEntry.FieldOrder = tempProp.inspectorOrder;
|
||||
|
||||
targetEntry.PrefabOverrideName ??= tempProp.uiPropertyName;
|
||||
if (entry is XuiFieldTempEntry tempEntry)
|
||||
{
|
||||
var matched = bindingCollection.FindByMemberName(entry.EntryName);
|
||||
if (matched != null)
|
||||
tempEntry.AddBinding(matched, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理其他旧的验证 Attribute
|
||||
foreach (var attr in memberInfo.GetCustomAttributes().OfType<IXuiavrgAttribute>())
|
||||
{
|
||||
if (attr is XuiavrgAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgHideAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldEntryGroupAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldOrderAttribute) continue; // 已处理
|
||||
if (attr is XuiavrgFieldPrefabOverrideAttribute) continue; // 已处理
|
||||
|
||||
// 其他旧 Attribute(Required, StringCheck, NumberCheck 等)
|
||||
var tempProp = new XuiavrgInspectorProperty();
|
||||
tempProp.RegsiterDataValue(memberInfo, payload);
|
||||
attr.BindProperty(tempProp);
|
||||
// 这些验证信息目前由 InspectorProperty 保留,后续可迁移到 Entry
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成后期回调(可重写)
|
||||
/// 在此方法中可以手动添加额外的 Entry(如 Preview 结构、不在数据对象中的内容)
|
||||
/// </summary>
|
||||
/// <param name="entries">当前 Entry 列表</param>
|
||||
protected virtual void OnPostBuildEntries(List<IFieldEntry> entries)
|
||||
{
|
||||
// 默认无操作,子类可重写
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对 Entry 列表按 FieldOrder 排序
|
||||
/// </summary>
|
||||
private void SortEntries()
|
||||
{
|
||||
CurrentEntries.Sort((a, b) =>
|
||||
@@ -304,106 +170,181 @@ namespace XericUI.ReflectionUIGenerator
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 三阶段 GenerateUI ============
|
||||
|
||||
/// <summary>
|
||||
/// 根据 Entry 列表生成 UI
|
||||
/// 生成 UI — 三阶段流程
|
||||
/// Phase A: 按 GroupPath 分组
|
||||
/// Phase B: 创建 XuiAvrgMDGroup 组容器
|
||||
/// Phase C: 在组容器内生成 UI 组件并绑定
|
||||
/// </summary>
|
||||
public void GenerateUI()
|
||||
{
|
||||
if (CurrentEntries == null || CurrentEntries.Count == 0)
|
||||
{
|
||||
Debug.LogWarning("[Xuiavrg] 没有可生成的 Entry,请先调用 BuildEntries");
|
||||
Debug.LogWarning("[Xuiavrg] 没有可生成的 Entry,请先调用 BuildFromMetaDataSet");
|
||||
return;
|
||||
}
|
||||
|
||||
// Phase A: 收集并分组
|
||||
var groupedEntries = new Dictionary<string, List<IFieldEntry>>();
|
||||
var ungroupedEntries = new List<IFieldEntry>();
|
||||
|
||||
foreach (var entry in CurrentEntries)
|
||||
{
|
||||
if (entry is not XuiFieldTempEntry tempEntry) continue;
|
||||
string groupKey = !string.IsNullOrEmpty(entry.EntryName)
|
||||
? GetGroupPathFromEntry(entry)
|
||||
: null;
|
||||
|
||||
// 查找匹配的 PropertyGroup
|
||||
var group = ResolvePropertyGroup(entry);
|
||||
if (group == null)
|
||||
if (!string.IsNullOrEmpty(groupKey))
|
||||
{
|
||||
Debug.LogWarning($"[Xuiavrg] 未找到 Entry '{entry.EntryName}' 的匹配预制体组,跳过");
|
||||
continue;
|
||||
if (!groupedEntries.ContainsKey(groupKey))
|
||||
groupedEntries[groupKey] = new List<IFieldEntry>();
|
||||
groupedEntries[groupKey].Add(entry);
|
||||
}
|
||||
else
|
||||
{
|
||||
ungroupedEntries.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
entry.BoundPrefabGroup = group;
|
||||
// Phase B: 创建组容器
|
||||
_activeGroups.Clear();
|
||||
foreach (var kvp in groupedEntries)
|
||||
{
|
||||
string groupPath = kvp.Key;
|
||||
var entries = kvp.Value;
|
||||
|
||||
// 从对象池获取 UI 实例
|
||||
var uiInstance = PropertyGroupPoolGet(group);
|
||||
if (uiInstance == null) continue;
|
||||
// 检查是否已创建(相同 GroupPath 只创建一个容器)
|
||||
if (_activeGroups.ContainsKey(groupPath)) continue;
|
||||
|
||||
// 设置父节点和命名
|
||||
uiInstance.name = entry.EntryName;
|
||||
uiInstance.transform.SetParent(transform, false);
|
||||
var groupGo = new GameObject($"Group [{groupPath}]", typeof(RectTransform));
|
||||
groupGo.transform.SetParent(transform, false);
|
||||
|
||||
// 绑定 UI 组件(首次绑定不传缓存,由 Entry 内部收集)
|
||||
entry.BindUIComponent(uiInstance, null);
|
||||
var rect = groupGo.GetComponent<RectTransform>();
|
||||
rect.anchorMin = new Vector2(0, 1);
|
||||
rect.anchorMax = new Vector2(1, 1);
|
||||
rect.pivot = new Vector2(0.5f, 1);
|
||||
rect.sizeDelta = new Vector2(0, 0);
|
||||
|
||||
// 绑定事件
|
||||
entry.OnEntryBind();
|
||||
var group = groupGo.AddComponent<XuiAvrgMDGroup>();
|
||||
int firstOrder = entries.Count > 0 ? entries[0].FieldOrder : 0;
|
||||
group.Initialize(groupPath, firstOrder);
|
||||
|
||||
_activeGroups[groupPath] = group;
|
||||
}
|
||||
|
||||
// Phase C: 生成 UI 组件
|
||||
foreach (var kvp in groupedEntries)
|
||||
{
|
||||
string groupPath = kvp.Key;
|
||||
var entries = kvp.Value;
|
||||
|
||||
if (!_activeGroups.TryGetValue(groupPath, out var group)) continue;
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
GenerateSingleUI(entry, group.contentRoot);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var entry in ungroupedEntries)
|
||||
{
|
||||
GenerateSingleUI(entry, transform);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Entry 对应的 PropertyGroup
|
||||
/// 优先使用 PrefabOverrideName,否则按 Tag 匹配,所有查找在 currentTheme 下进行
|
||||
/// 生成单个 Entry 的 UI
|
||||
/// </summary>
|
||||
private XuiavrgPropertyGroupScriptableObject ResolvePropertyGroup(IFieldEntry entry)
|
||||
private void GenerateSingleUI(IFieldEntry entry, Transform parent)
|
||||
{
|
||||
if (entry is not XuiFieldTempEntry tempEntry) return;
|
||||
|
||||
var prefab = ResolvePrefab(entry);
|
||||
if (prefab == null)
|
||||
{
|
||||
Debug.LogWarning($"[Xuiavrg] 未找到 Entry '{entry.EntryName}' 的匹配预制体,跳过");
|
||||
return;
|
||||
}
|
||||
|
||||
entry.BoundPrefab = prefab;
|
||||
var uiInstance = PrefabPoolGet(prefab);
|
||||
|
||||
if (uiInstance == null) return;
|
||||
|
||||
uiInstance.name = entry.EntryName;
|
||||
uiInstance.transform.SetParent(parent, false);
|
||||
|
||||
// 检查 XUIAvrgMDValue 是否存在
|
||||
if (uiInstance.GetComponentInChildren<XUIAvrgMDValue>(true) == null)
|
||||
{
|
||||
Debug.LogError($"[Xuiavrg] 预制体 '{prefab.name}' 不符合生成要求:缺少 XUIAvrgMDValue 组件。请确保预制体根节点或其子节点包含 XUIAvrgMDValue 脚本。");
|
||||
PrefabPoolRelease(prefab, uiInstance);
|
||||
return;
|
||||
}
|
||||
|
||||
entry.BindUIComponent(uiInstance, null);
|
||||
entry.OnEntryBind();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Entry 提取 GroupPath(通过 EntryName 判断)
|
||||
/// </summary>
|
||||
private string GetGroupPathFromEntry(IFieldEntry entry)
|
||||
{
|
||||
// 通过 Bindings 中的元数据找到 GroupPath
|
||||
if (entry.Bindings != null && entry.Bindings.Count > 0)
|
||||
{
|
||||
var metaData = entry.Bindings[0].MetaData;
|
||||
if (metaData != null && !string.IsNullOrEmpty(metaData.GroupPath))
|
||||
return metaData.GroupPath;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============ 预制体解析 ============
|
||||
|
||||
private GameObject ResolvePrefab(IFieldEntry entry)
|
||||
{
|
||||
// 1. 优先检查 PrefabOverrideName(按主题 + UI 名称查找)
|
||||
if (!string.IsNullOrEmpty(entry.PrefabOverrideName))
|
||||
{
|
||||
var overrideGroup = XuiavrgPropertyGroupScriptableObject.FindGroupByUiName(_currentTheme, entry.PrefabOverrideName);
|
||||
if (overrideGroup != null) return overrideGroup;
|
||||
}
|
||||
|
||||
// 2. 按 Tag 查找匹配的组(在当前主题下)
|
||||
if (entry.Tag != XuiavrgFieldTag.None)
|
||||
{
|
||||
var tags = new List<XuiavrgFieldTag> { entry.Tag };
|
||||
// 收集子 Entry 的 Tag(如果是编组 Entry)
|
||||
if (entry.IsGroupEntry)
|
||||
if (typePrefabMappingConfig != null)
|
||||
{
|
||||
foreach (var child in entry.Children)
|
||||
{
|
||||
if (child.Tag != XuiavrgFieldTag.None && !tags.Contains(child.Tag))
|
||||
tags.Add(child.Tag);
|
||||
}
|
||||
var namedPrefab = typePrefabMappingConfig.FindPrefabByName(entry.PrefabOverrideName);
|
||||
if (namedPrefab != null) return namedPrefab;
|
||||
}
|
||||
|
||||
var matchedGroup = XuiavrgPropertyGroupScriptableObject.FindGroup(_currentTheme, tags.ToArray());
|
||||
if (matchedGroup != null) return matchedGroup;
|
||||
}
|
||||
|
||||
// 3. 回退到当前主题下的 "Default" UI 名称
|
||||
return XuiavrgPropertyGroupScriptableObject.FindGroupByUiName(_currentTheme, "Default");
|
||||
if (typePrefabMappingConfig != null)
|
||||
{
|
||||
var typeMatch = typePrefabMappingConfig.FindPrefabByBindings(entry.Bindings);
|
||||
if (typeMatch != null) return typeMatch;
|
||||
}
|
||||
|
||||
return typePrefabMappingConfig != null ? typePrefabMappingConfig.defaultPrefab : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空当前界面和 Entry 列表
|
||||
/// </summary>
|
||||
// ============ 清理 ============
|
||||
|
||||
public void ClearAll()
|
||||
{
|
||||
ClearCurrentEntries();
|
||||
ClearAllPropertyGroupPools();
|
||||
CurrentPayload = null;
|
||||
ClearAllPrefabPools();
|
||||
ClearGroups();
|
||||
CurrentMetaDataSet = null;
|
||||
CurrentBindingCollection = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空当前 Entry 列表(归还到池中)
|
||||
/// </summary>
|
||||
private void ClearCurrentEntries()
|
||||
{
|
||||
foreach (var entry in CurrentEntries)
|
||||
{
|
||||
// 释放 UI 实例
|
||||
if (entry.UIInstance != null && entry.BoundPrefabGroup != null)
|
||||
if (entry.UIInstance != null && entry.BoundPrefab != null)
|
||||
{
|
||||
PropertyGroupPoolRelease(entry.BoundPrefabGroup, entry.UIInstance);
|
||||
PrefabPoolRelease(entry.BoundPrefab, entry.UIInstance);
|
||||
}
|
||||
|
||||
// 归还 Entry 到池
|
||||
if (entry is XuiFieldTempEntry tempEntry)
|
||||
{
|
||||
XuiFieldTempEntry.Release(tempEntry);
|
||||
@@ -412,29 +353,35 @@ namespace XericUI.ReflectionUIGenerator
|
||||
CurrentEntries.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
private void ClearGroups()
|
||||
{
|
||||
foreach (var kvp in _activeGroups)
|
||||
{
|
||||
if (kvp.Value != null && kvp.Value.gameObject != null)
|
||||
{
|
||||
Destroy(kvp.Value.gameObject);
|
||||
}
|
||||
}
|
||||
_activeGroups.Clear();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ClearAll();
|
||||
}
|
||||
|
||||
#endregion
|
||||
// ============ 兼容旧 API ============
|
||||
|
||||
#region 兼容旧 API
|
||||
|
||||
/// <summary>
|
||||
/// [已弃用] 请使用 BuildEntries + GenerateUI 替代
|
||||
/// </summary>
|
||||
[Obsolete("Use BuildEntries + GenerateUI instead", false)]
|
||||
[Obsolete("Use BuildFromMetaDataSet + GenerateUI instead", false)]
|
||||
public void SchemaHydration(object payload)
|
||||
{
|
||||
BuildEntries(payload);
|
||||
var metaDataSet = XuiavrgUtilities.ExtractAvrgMemberMetaData(payload);
|
||||
var bindingCollection = metaDataSet.BuildMDVM(payload);
|
||||
BuildFromMetaDataSet(metaDataSet, bindingCollection);
|
||||
GenerateUI();
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 反射属性中间层
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class XuiavrgInspectorProperty
|
||||
{
|
||||
#region 字段属性
|
||||
|
||||
public object valueTarget;
|
||||
public bool isProperty;
|
||||
public PropertyInfo propertyInfo;
|
||||
public FieldInfo fieldInfo;
|
||||
|
||||
// 基本信息
|
||||
public string labelName; // 字段名称
|
||||
public string uiPropertyName; // ui属性名称
|
||||
public string unitLabelName; // 单位名称
|
||||
public bool required; // 项目必填
|
||||
public bool notEmpty; // 不可为空
|
||||
public int inspectorOrder; // 显示在检查器上的顺序
|
||||
public string groupName; // 编组名称
|
||||
public string onTriggerCall; // 值变更时触发的反射
|
||||
public XuiavrgSize argumentSize; // 属性尺寸
|
||||
public bool hideInInspector; // 在检查器上隐藏显示
|
||||
|
||||
// 字符串检查
|
||||
public int stringMinLength; // 最小长度
|
||||
public int stringMaxLength = int.MaxValue; // 最大长度
|
||||
public string stringRegexPattern = ""; // 正则检查
|
||||
public bool stringOnlyLetters; // 允许字符
|
||||
public bool stringOnlyNumber; // 允许数字
|
||||
public bool stringOnlyEmail = true; // 允许邮箱
|
||||
public bool stringOnlyPhone = true; // 允许电话号码
|
||||
public bool stringOnlyUrl = true; // 允许网址
|
||||
|
||||
// 数值检查
|
||||
public float numberMinValue; // 最小值
|
||||
public float numberMaxValue; // 最大值
|
||||
|
||||
// 集合检查
|
||||
public int collectionMinCount; // 集合的最小尺寸
|
||||
public int collectionMaxCount = int.MaxValue; // 集合的最大尺寸
|
||||
public bool collectionNotEmpty; // 集合不可为空
|
||||
|
||||
#endregion
|
||||
|
||||
#region 检查方法
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到数据端
|
||||
/// </summary>
|
||||
/// <param name="memberInfo"></param>
|
||||
/// <param name="target"></param>
|
||||
public void RegsiterDataValue(MemberInfo memberInfo, object target)
|
||||
{
|
||||
switch (memberInfo)
|
||||
{
|
||||
case PropertyInfo propertyInfo:
|
||||
RegsiterDataValue(propertyInfo, target);
|
||||
break;
|
||||
case FieldInfo fieldInfo:
|
||||
RegsiterDataValue(fieldInfo, target);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到数据端
|
||||
/// </summary>
|
||||
/// <param name="memberInfo"></param>
|
||||
/// <param name="target"></param>
|
||||
public virtual void RegsiterDataValue(PropertyInfo memberInfo, object target)
|
||||
{
|
||||
valueTarget = target;
|
||||
isProperty = true;
|
||||
propertyInfo = memberInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到数据端
|
||||
/// </summary>
|
||||
/// <param name="memberInfo"></param>
|
||||
/// <param name="target"></param>
|
||||
public virtual void RegsiterDataValue(FieldInfo memberInfo, object target)
|
||||
{
|
||||
valueTarget = target;
|
||||
isProperty = false;
|
||||
fieldInfo = memberInfo;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 绑定到界面端
|
||||
/// </summary>
|
||||
public virtual void RegsiterUIElement()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 数据端值发生变更
|
||||
/// </summary>
|
||||
public void CallDataValueChange()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 界面端数据发生变更
|
||||
/// </summary>
|
||||
public void CallUIValueChange()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从数据端获取值
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public object GetValueFromTargetMember()
|
||||
{
|
||||
if (isProperty)
|
||||
return propertyInfo.GetValue(valueTarget);
|
||||
else
|
||||
return fieldInfo.GetValue(valueTarget);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值到数据端
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetValueToTargetMember(object value)
|
||||
{
|
||||
if (isProperty)
|
||||
propertyInfo.SetValue(valueTarget, value);
|
||||
else
|
||||
fieldInfo.SetValue(valueTarget, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从界面端获取值
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public object GetValueFormUIElement()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值到界面端
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
public void SetValueToUIElement(object value)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d82ae110adb48b397c09939694cc3b2
|
||||
timeCreated: 1777011022
|
||||
@@ -1,309 +0,0 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
#if ODIN_INSPECTOR
|
||||
using Sirenix.OdinInspector;
|
||||
#endif
|
||||
using UnityEngine;
|
||||
using UnityEngine.Serialization;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// ui反射属性定位器,放在UI对象的根节点上,设定好接下来的目标即可
|
||||
/// </summary>
|
||||
public class XuiavrgLocator : MonoBehaviour
|
||||
{
|
||||
public delegate void AnyTrigger(LocatorProperty locator, object value, int selectStart, int selectEnd);
|
||||
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("映射组件")]
|
||||
#endif
|
||||
public List<LocatorProperty> Properties;
|
||||
|
||||
[Serializable]
|
||||
public class LocatorProperty
|
||||
{
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("组件标识名")]
|
||||
#endif
|
||||
[Tooltip("建议使用 name, label, unit, sign, input, output, button, slider, edit, select 等简单易懂的名称进行标记")]
|
||||
public string propertyName;
|
||||
#if ODIN_INSPECTOR
|
||||
[LabelText("组件类型")]
|
||||
#endif
|
||||
public UIElementClassify classify;
|
||||
#if dUI_TextMeshPro
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.TMPText)]
|
||||
#endif
|
||||
public TMPro.TMP_Text TMPText;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.TMPInputField)]
|
||||
#endif
|
||||
public TMPro.TMP_InputField TMPInputField;
|
||||
#endif
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIText)]
|
||||
#endif
|
||||
public UnityEngine.UI.Text UIText;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIInputField)]
|
||||
#endif
|
||||
public UnityEngine.UI.InputField UIInputField;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIToggle)]
|
||||
#endif
|
||||
public UnityEngine.UI.Toggle UIToggle;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIButton)]
|
||||
#endif
|
||||
public UnityEngine.UI.Button UIButton;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UISlider)]
|
||||
#endif
|
||||
public UnityEngine.UI.Slider UISlider;
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIDropdown)]
|
||||
#endif
|
||||
public UnityEngine.UI.Dropdown UIDropdown;
|
||||
#if dUI_TextMeshPro
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.TMPInputField)]
|
||||
#endif
|
||||
public TMPInputTriggerEvent TMPInputTriggerOnEventType;
|
||||
#endif
|
||||
#if ODIN_INSPECTOR
|
||||
[ShowIf("classify", UIElementClassify.UIInputField)]
|
||||
#endif
|
||||
public UIInputTriggerEvent UIInputTriggerOnEventType;
|
||||
|
||||
/// <summary>
|
||||
/// 支持的组件类型
|
||||
/// </summary>
|
||||
public enum UIElementClassify
|
||||
{
|
||||
None,
|
||||
#if dUI_TextMeshPro
|
||||
TMPText,
|
||||
TMPInputField,
|
||||
#endif
|
||||
UIText,
|
||||
UIInputField,
|
||||
UIToggle,
|
||||
UIButton,
|
||||
UISlider,
|
||||
UIDropdown,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum TMPInputTriggerEvent
|
||||
{
|
||||
none,
|
||||
onEndEdit = 1 << 1,
|
||||
onSubmit = 1 << 2,
|
||||
onSelect = 1 << 3,
|
||||
onDeselect = 1 << 4,
|
||||
onTextSelection = 1 << 5,
|
||||
onEndTextSelection = 1 << 6,
|
||||
onValueChanged = 1 << 7,
|
||||
onTouchScreenKeyboardStatusChanged = 1 << 8,
|
||||
}
|
||||
|
||||
public enum UIInputTriggerEvent
|
||||
{
|
||||
none,
|
||||
onEndEdit,
|
||||
onSubmit,
|
||||
onValueChanged,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取值
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public object GetValue()
|
||||
{
|
||||
switch (classify)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
case UIElementClassify.TMPText:
|
||||
return TMPText.text;
|
||||
case UIElementClassify.TMPInputField:
|
||||
return TMPInputField.text;
|
||||
#endif
|
||||
case UIElementClassify.UIText:
|
||||
return UIText.text;
|
||||
case UIElementClassify.UIInputField:
|
||||
return UIInputField.text;
|
||||
case UIElementClassify.UIToggle:
|
||||
return UIToggle.isOn;
|
||||
case UIElementClassify.UIButton:
|
||||
return false;
|
||||
case UIElementClassify.UISlider:
|
||||
return UISlider.value;
|
||||
case UIElementClassify.UIDropdown:
|
||||
return UIDropdown.value;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置值
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <exception cref="ArgumentOutOfRangeException"></exception>
|
||||
public void SetValue(object value)
|
||||
{
|
||||
switch (classify)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
case UIElementClassify.TMPText:
|
||||
TMPText.text = value.ToString();
|
||||
break;
|
||||
case UIElementClassify.TMPInputField:
|
||||
TMPInputField.text = value.ToString();
|
||||
break;
|
||||
#endif
|
||||
case UIElementClassify.UIText:
|
||||
UIText.text = value.ToString();
|
||||
break;
|
||||
case UIElementClassify.UIInputField:
|
||||
UIInputField.text = value.ToString();
|
||||
break;
|
||||
case UIElementClassify.UIToggle:
|
||||
UIToggle.isOn = Convert.ToBoolean(value);
|
||||
break;
|
||||
case UIElementClassify.UIButton:
|
||||
// UIButton do nothing
|
||||
break;
|
||||
case UIElementClassify.UISlider:
|
||||
UISlider.value = Convert.ToSingle(value);
|
||||
break;
|
||||
case UIElementClassify.UIDropdown:
|
||||
UIDropdown.value = Convert.ToInt32(value);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ui触发事件
|
||||
/// </summary>
|
||||
public event AnyTrigger OnUITrigger
|
||||
{
|
||||
add
|
||||
{
|
||||
switch (classify)
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
case UIElementClassify.TMPInputField:
|
||||
switch (TMPInputTriggerOnEventType)
|
||||
{
|
||||
case TMPInputTriggerEvent.onEndEdit:
|
||||
TMPInputField.onEndEdit.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onSubmit:
|
||||
TMPInputField.onSubmit.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onSelect:
|
||||
TMPInputField.onSelect.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onDeselect:
|
||||
TMPInputField.onDeselect.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onTextSelection:
|
||||
TMPInputField.onTextSelection.AddListener((content, start, end) =>
|
||||
value?.Invoke(this, content, start, end));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onEndTextSelection:
|
||||
TMPInputField.onEndTextSelection.AddListener((content, start, end) =>
|
||||
value?.Invoke(this, content, start, end));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onValueChanged:
|
||||
TMPInputField.onValueChanged.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case TMPInputTriggerEvent.onTouchScreenKeyboardStatusChanged:
|
||||
TMPInputField.onTouchScreenKeyboardStatusChanged.AddListener(a =>
|
||||
value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
#endif
|
||||
case UIElementClassify.UIInputField:
|
||||
switch (UIInputTriggerOnEventType)
|
||||
{
|
||||
case UIInputTriggerEvent.onEndEdit:
|
||||
UIInputField.onEndEdit.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case UIInputTriggerEvent.onSubmit:
|
||||
UIInputField.onSubmit.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case UIInputTriggerEvent.onValueChanged:
|
||||
UIInputField.onValueChanged.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
break;
|
||||
case UIElementClassify.UIToggle:
|
||||
UIToggle.onValueChanged.AddListener(a => value?.Invoke(this, value, 0, 0));
|
||||
break;
|
||||
case UIElementClassify.UIButton:
|
||||
UIButton.onClick.AddListener(() => value?.Invoke(this, true, 0, 0));
|
||||
break;
|
||||
case UIElementClassify.UISlider:
|
||||
UISlider.onValueChanged.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
case UIElementClassify.UIDropdown:
|
||||
UIDropdown.onValueChanged.AddListener(a => value?.Invoke(this, a, 0, 0));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
remove { Debug.Log("任意触发器无法卸载特定事件"); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清空ui触发事件
|
||||
/// </summary>
|
||||
public void ClearTriggerEvent()
|
||||
{
|
||||
#if dUI_TextMeshPro
|
||||
if (TMPInputField)
|
||||
{
|
||||
TMPInputField.onEndEdit.RemoveAllListeners();
|
||||
TMPInputField.onSubmit.RemoveAllListeners();
|
||||
TMPInputField.onSelect.RemoveAllListeners();
|
||||
TMPInputField.onDeselect.RemoveAllListeners();
|
||||
TMPInputField.onTextSelection.RemoveAllListeners();
|
||||
TMPInputField.onEndTextSelection.RemoveAllListeners();
|
||||
TMPInputField.onValueChanged.RemoveAllListeners();
|
||||
TMPInputField.onTouchScreenKeyboardStatusChanged.RemoveAllListeners();
|
||||
}
|
||||
#endif
|
||||
if (UIInputField)
|
||||
{
|
||||
UIInputField.onEndEdit.RemoveAllListeners();
|
||||
UIInputField.onSubmit.RemoveAllListeners();
|
||||
UIInputField.onValueChanged.RemoveAllListeners();
|
||||
}
|
||||
|
||||
UIToggle?.onValueChanged.RemoveAllListeners();
|
||||
UIButton?.onClick.RemoveAllListeners();
|
||||
UISlider?.onValueChanged.RemoveAllListeners();
|
||||
UIDropdown?.onValueChanged.RemoveAllListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// todo 使用名称,类型筛选得到组件
|
||||
// todo 根据名称组建字典树加速查找(或许不用)
|
||||
}
|
||||
}
|
||||
-299
@@ -1,299 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// 界面属性编组
|
||||
///
|
||||
/// 将一种 C# 数据类型与一个 UI 预制体绑定,并归属于一个主题。
|
||||
/// 类似 Newtonsoft.Json 的 Converter 概念 — 针对特定 Type,指定特定的 UI 模板。
|
||||
///
|
||||
/// 同一类型可以有多个不同 uiName/主题的重载(如 "简单输入框" vs "带标签的输入框")。
|
||||
/// Manager 根据当前主题名 + 字段 Tag 自动匹配对应的编组。
|
||||
///
|
||||
/// 全局注册表以 themeName 为键分组管理,OnEnable/OnDisable 自动注册/注销。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[CreateAssetMenu(fileName = "Xeric UI avr Property group", menuName = "Xeric Library/UI Action Vessel/Reflection UI Gen/UI avr Property Group", order = 10)]
|
||||
public class XuiavrgPropertyGroupScriptableObject : ScriptableObject
|
||||
{
|
||||
#region 全局注册表
|
||||
|
||||
/// <summary>
|
||||
/// 全局预制体组字典,以主题名称为键分组
|
||||
/// </summary>
|
||||
internal static Dictionary<string, List<XuiavrgPropertyGroupScriptableObject>> GlobalPrefabDict
|
||||
= new Dictionary<string, List<XuiavrgPropertyGroupScriptableObject>>();
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题名获取该主题下所有编组
|
||||
/// </summary>
|
||||
public static List<XuiavrgPropertyGroupScriptableObject> GetGroupsByTheme(string themeName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(themeName))
|
||||
themeName = DefaultThemeName;
|
||||
|
||||
GlobalPrefabDict.TryGetValue(themeName, out var list);
|
||||
return list ?? new List<XuiavrgPropertyGroupScriptableObject>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题名 + 标记查找编组
|
||||
/// </summary>
|
||||
public static XuiavrgPropertyGroupScriptableObject FindGroup(string themeName, params XuiavrgFieldTag[] tags)
|
||||
{
|
||||
var groups = GetGroupsByTheme(themeName);
|
||||
return groups.FirstOrDefault(g => tags.All(t => g.supportedTags.Contains(t)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题名 + UI 名称查找编组
|
||||
/// </summary>
|
||||
public static XuiavrgPropertyGroupScriptableObject FindGroupByUiName(string themeName, string uiName)
|
||||
{
|
||||
var groups = GetGroupsByTheme(themeName);
|
||||
return groups.FirstOrDefault(g => g.uiName == uiName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据主题名 + 绑定类型查找所有匹配的编组
|
||||
/// </summary>
|
||||
public static List<XuiavrgPropertyGroupScriptableObject> FindGroupsByType(string themeName, string valueTypeName)
|
||||
{
|
||||
var groups = GetGroupsByTheme(themeName);
|
||||
return groups.Where(g => g.valueTypeName == valueTypeName).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 默认主题名称
|
||||
/// </summary>
|
||||
public const string DefaultThemeName = "Default";
|
||||
|
||||
#endregion
|
||||
|
||||
#region 属性字段
|
||||
|
||||
/// <summary>
|
||||
/// 主题名称。同一主题下的 UI 组件共享统一风格。
|
||||
/// </summary>
|
||||
[Tooltip("主题名称。同一主题下的 UI 组件共享统一风格。Manager 根据其 currentTheme 查找对应编组")]
|
||||
public string themeName = DefaultThemeName;
|
||||
|
||||
/// <summary>
|
||||
/// UI 个体名称。同一类型可有多个不同名称的重载。
|
||||
/// 在数据字段上通过 XuiavrgFieldPrefabOverride 按此名称指定。
|
||||
/// </summary>
|
||||
[Tooltip("UI 个体名称。用于 XuiavrgFieldPrefabOverride 按名指定,同一类型可有多个重载")]
|
||||
public string uiName = "Default";
|
||||
|
||||
/// <summary>
|
||||
/// 此编组绑定的 C# 数据类型全名(如 System.Single、System.String)
|
||||
/// </summary>
|
||||
[Tooltip("绑定的 C# 数据类型全名,如 System.Single、System.Boolean")]
|
||||
public string valueTypeName = "System.Single";
|
||||
|
||||
/// <summary>
|
||||
/// 解析后的类型缓存
|
||||
/// </summary>
|
||||
public Type ValueType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_valueType != null) return _valueType;
|
||||
if (!string.IsNullOrEmpty(valueTypeName))
|
||||
_valueType = Type.GetType(valueTypeName);
|
||||
return _valueType;
|
||||
}
|
||||
}
|
||||
private Type _valueType;
|
||||
|
||||
/// <summary>
|
||||
/// 所有程序集中类型名的缓存 — 简单名列表(如 "Boolean"、"Int32"、"String")
|
||||
/// 与 _allTypeFullNames 下标一一对应
|
||||
/// </summary>
|
||||
private static List<string> _allTypeSimpleNames;
|
||||
|
||||
/// <summary>
|
||||
/// 所有程序集中类型名的缓存 — 全名列表(如 "System.Boolean"、"System.Int32")
|
||||
/// 与 _allTypeSimpleNames 下标一一对应
|
||||
/// </summary>
|
||||
private static List<string> _allTypeFullNames;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化类型名缓存 — 反射扫描所有已加载程序集中的类型
|
||||
/// </summary>
|
||||
private static void EnsureTypeCacheBuilt()
|
||||
{
|
||||
if (_allTypeFullNames != null) return;
|
||||
|
||||
_allTypeSimpleNames = new List<string>();
|
||||
_allTypeFullNames = new List<string>();
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
|
||||
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
try
|
||||
{
|
||||
foreach (var type in asm.GetTypes())
|
||||
{
|
||||
var fullName = type.FullName;
|
||||
if (string.IsNullOrEmpty(fullName)) continue;
|
||||
// 跳过泛型参数位置和编译器生成的类型
|
||||
if (fullName.Contains("`") || fullName.Contains("<")) continue;
|
||||
|
||||
if (!seen.Add(fullName)) continue;
|
||||
|
||||
_allTypeSimpleNames.Add(type.Name);
|
||||
_allTypeFullNames.Add(fullName);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 某些程序集可能无法反射(如动态程序集),忽略
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将用户输入的类型名纠正为标准全名。
|
||||
/// 通过 LevenshteinDistance 在所有已知类型中找到最匹配的名称。
|
||||
/// 如 "boolean" → "System.Boolean", "single" → "System.Single"。
|
||||
/// 如果已经是标准全名或无法匹配则返回原值(不做修改)。
|
||||
/// </summary>
|
||||
/// <param name="userInput">用户输入的类型名</param>
|
||||
/// <returns>纠正后的标准类型全名,无法识别时返回原值</returns>
|
||||
public static string NormalizeTypeName(string userInput)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userInput))
|
||||
return userInput;
|
||||
|
||||
userInput = userInput.Trim();
|
||||
|
||||
// 已经是标准全名(包含 "."),直接视为 OK,不做修改
|
||||
if (userInput.Contains('.'))
|
||||
return userInput;
|
||||
|
||||
// 初始化类型缓存
|
||||
EnsureTypeCacheBuilt();
|
||||
|
||||
// 通过 Levenshtein 距离在所有类型简单名中找到最匹配的
|
||||
var bestMatch = XericLibrary.Runtime.MacroLibrary.MacroMath.LevenshteinDistance(
|
||||
_allTypeSimpleNames, userInput,
|
||||
out int dis, out int index);
|
||||
|
||||
// 阈值:距离超过输入长度 2 倍 + 2,认为不匹配,返回原值
|
||||
if (bestMatch == null || dis > userInput.Length * 2 + 2)
|
||||
return userInput;
|
||||
|
||||
// 返回对应的全名
|
||||
return _allTypeFullNames[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将此实例的 valueTypeName 纠正为标准全名
|
||||
/// </summary>
|
||||
public void NormalizeValueTypeName()
|
||||
{
|
||||
valueTypeName = NormalizeTypeName(valueTypeName);
|
||||
_valueType = null; // 清空缓存,下次访问时重新解析
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对应的 UI 预制体
|
||||
/// </summary>
|
||||
[Tooltip("拖入 UI 预制体,系统会自动扫描其中的 IXuiavrgFieldTagProvider 组件")]
|
||||
public GameObject prefab;
|
||||
|
||||
/// <summary>
|
||||
/// 此编组支持的 UI 功能标记集合
|
||||
/// </summary>
|
||||
[Tooltip("此编组包含的 UI 功能标记。Manager 根据字段 Tag 匹配对应编组")]
|
||||
public List<XuiavrgFieldTag> supportedTags = new List<XuiavrgFieldTag>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region 组件自动识别(运行时 + Editor 共用)
|
||||
|
||||
/// <summary>
|
||||
/// 从预制体中自动扫描到的 IXuiavrgFieldTagProvider 组件列表(非序列化,仅供 Editor 展示)
|
||||
/// </summary>
|
||||
[NonSerialized]
|
||||
public List<Component> detectedComponents = new List<Component>();
|
||||
|
||||
/// <summary>
|
||||
/// 重新扫描预制体中的标记组件
|
||||
/// </summary>
|
||||
public void ScanPrefabComponents()
|
||||
{
|
||||
detectedComponents.Clear();
|
||||
if (prefab == null) return;
|
||||
|
||||
var components = prefab.GetComponentsInChildren<Component>(true);
|
||||
foreach (var comp in components)
|
||||
{
|
||||
if (comp is IXuiavrgFieldTagProvider tagProvider && tagProvider.EntryTag != XuiavrgFieldTag.None)
|
||||
{
|
||||
detectedComponents.Add(comp);
|
||||
}
|
||||
}
|
||||
|
||||
// 自动同步 supportedTags
|
||||
HashSet<XuiavrgFieldTag> autoTags = new HashSet<XuiavrgFieldTag>();
|
||||
foreach (var comp in detectedComponents)
|
||||
{
|
||||
if (comp is IXuiavrgFieldTagProvider tp && tp.EntryTag != XuiavrgFieldTag.None)
|
||||
autoTags.Add(tp.EntryTag);
|
||||
}
|
||||
|
||||
foreach (var tag in autoTags)
|
||||
{
|
||||
if (!supportedTags.Contains(tag))
|
||||
supportedTags.Add(tag);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 生命周期
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
RegisterToGlobalDict();
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
UnregisterFromGlobalDict();
|
||||
}
|
||||
|
||||
private void RegisterToGlobalDict()
|
||||
{
|
||||
string key = string.IsNullOrEmpty(themeName) ? DefaultThemeName : themeName;
|
||||
if (!GlobalPrefabDict.TryGetValue(key, out var list))
|
||||
{
|
||||
list = new List<XuiavrgPropertyGroupScriptableObject>();
|
||||
GlobalPrefabDict[key] = list;
|
||||
}
|
||||
if (!list.Contains(this))
|
||||
list.Add(this);
|
||||
}
|
||||
|
||||
private void UnregisterFromGlobalDict()
|
||||
{
|
||||
string key = string.IsNullOrEmpty(themeName) ? DefaultThemeName : themeName;
|
||||
if (GlobalPrefabDict.TryGetValue(key, out var list))
|
||||
{
|
||||
list.Remove(this);
|
||||
if (list.Count == 0)
|
||||
GlobalPrefabDict.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bcd1c64483d2457ab9143f086dff511d
|
||||
timeCreated: 1777024131
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace XericUI.ReflectionUIGenerator
|
||||
@@ -9,8 +10,6 @@ namespace XericUI.ReflectionUIGenerator
|
||||
/// <summary>
|
||||
/// 获取所有运行时成员(字段和属性)
|
||||
/// </summary>
|
||||
/// <param name="type"></param>
|
||||
/// <returns></returns>
|
||||
public static IEnumerable<MemberInfo> GetRuntimeMemberInfo(this Type type)
|
||||
{
|
||||
foreach (var field in type.GetRuntimeFields())
|
||||
@@ -18,5 +17,97 @@ namespace XericUI.ReflectionUIGenerator
|
||||
foreach (var property in type.GetRuntimeProperties())
|
||||
yield return property;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从任意对象提取 AvrgMemberMetaData 元数据集
|
||||
/// 反射识别成员、识别 IXuiavrgAttribute 标记、执行标记绑定的完整流程
|
||||
/// </summary>
|
||||
/// <param name="source">待提取的数据对象</param>
|
||||
/// <returns>提取完成的元数据集</returns>
|
||||
public static AvrgMemberMetaDataSet ExtractAvrgMemberMetaData(this object source)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
|
||||
var metaDataSet = new AvrgMemberMetaDataSet();
|
||||
var type = source.GetType();
|
||||
|
||||
foreach (var memberInfo in type.GetRuntimeMemberInfo())
|
||||
{
|
||||
// 检查隐藏标记
|
||||
var hideAttr = memberInfo.GetCustomAttribute<XuiavrgHideAttribute>();
|
||||
if (hideAttr != null && hideAttr.Hide)
|
||||
continue;
|
||||
|
||||
// 创建元数据
|
||||
var metaData = new AvrgMemberMetaData
|
||||
{
|
||||
MemberName = memberInfo.Name,
|
||||
MemberInfo = memberInfo,
|
||||
TargetInstance = source,
|
||||
};
|
||||
|
||||
// 识别值类型和 IsProperty
|
||||
switch (memberInfo)
|
||||
{
|
||||
case PropertyInfo pi:
|
||||
metaData.IsProperty = true;
|
||||
metaData.ValueType = pi.PropertyType;
|
||||
break;
|
||||
case FieldInfo fi:
|
||||
metaData.IsProperty = false;
|
||||
metaData.ValueType = fi.FieldType;
|
||||
break;
|
||||
}
|
||||
|
||||
// 提取字段默认值(类初始值)
|
||||
// Unity 序列化字段可以通过反射读取实例上的当前值作为"默认"参考
|
||||
// 但更准确的在类声明阶段的初始值需要通过运行时观察或 Fallback 判断
|
||||
// 此处策略:如果 TargetInstance 非 null,直接 ReadValue 作为 DefaultValue
|
||||
if (source != null && memberInfo is FieldInfo fi2)
|
||||
{
|
||||
metaData.DefaultValue = fi2.GetValue(source);
|
||||
}
|
||||
else if (source != null && memberInfo is PropertyInfo pi2 && pi2.CanRead)
|
||||
{
|
||||
metaData.DefaultValue = pi2.GetValue(source);
|
||||
}
|
||||
|
||||
// 设置 PropertyCheckKey 默认值(用类型全称)
|
||||
if (metaData.ValueType != null)
|
||||
metaData.PropertyCheckKey = metaData.ValueType.FullName;
|
||||
|
||||
// 收集所有 IXuiavrgAttribute
|
||||
var attributes = memberInfo.GetCustomAttributes()
|
||||
.OfType<IXuiavrgAttribute>()
|
||||
.ToList();
|
||||
|
||||
metaData.Attributes = attributes;
|
||||
|
||||
// 按序执行 BindMetaData
|
||||
foreach (var attr in attributes)
|
||||
{
|
||||
attr.BindMetaData(metaData);
|
||||
}
|
||||
|
||||
metaDataSet.Add(metaData);
|
||||
}
|
||||
|
||||
// 处理编组合并
|
||||
metaDataSet.MergeGroupEntries();
|
||||
|
||||
// 排序
|
||||
metaDataSet.SortByFieldOrder();
|
||||
|
||||
return metaDataSet;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从任意对象提取 AvrgMemberMetaData 元数据集的泛型版本
|
||||
/// </summary>
|
||||
public static AvrgMemberMetaDataSet ExtractAvrgMemberMetaData<T>(this T source)
|
||||
{
|
||||
return ExtractAvrgMemberMetaData((object)source);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user