优化ui绑定侧逻辑

This commit is contained in:
2026-07-25 23:45:29 +08:00
parent 3fbb82f40e
commit 251b8730b9
19 changed files with 782 additions and 436 deletions
@@ -0,0 +1,77 @@
using System.Collections.Generic;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 绑定名称前缀树(Trie),支持通过完整路径或成员名后缀快速查找 AvrgMvvmBinding。
///
/// 使用场景:
/// - Find("XericUI.MyData.speed") → 完整路径精确命中
/// - FindByMemberName("speed") → 沿 Trie 叶节点查找末段匹配
///
/// 在 BuildMDVM 时构建,绑定到 AvrgMvvmBindingCollection。
/// </summary>
internal class AvrgBindingTrie
{
private class Node
{
public AvrgMvvmBinding Binding;
public readonly Dictionary<string, Node> Children = new();
}
private readonly Node _root = new();
private readonly Dictionary<string, AvrgMvvmBinding> _memberNameIndex = new();
public void Insert(string fullPath, AvrgMvvmBinding binding)
{
if (string.IsNullOrEmpty(fullPath) || binding == null) return;
var parts = fullPath.Split('.');
var current = _root;
foreach (var part in parts)
{
if (!current.Children.TryGetValue(part, out var child))
{
child = new Node();
current.Children[part] = child;
}
current = child;
}
current.Binding = binding;
// 同时建成员名索引(末段)
string memberName = parts[parts.Length - 1];
if (!_memberNameIndex.ContainsKey(memberName))
_memberNameIndex[memberName] = binding;
}
/// <summary>通过完整路径精确查找(如 "XericUI.MyData.speed")</summary>
public AvrgMvvmBinding Find(string fullPath)
{
if (string.IsNullOrEmpty(fullPath)) return null;
var parts = fullPath.Split('.');
var current = _root;
foreach (var part in parts)
{
if (!current.Children.TryGetValue(part, out current))
return null;
}
return current?.Binding;
}
/// <summary>通过成员名查找(如 "speed")— O(1) 直接索引</summary>
public AvrgMvvmBinding FindByMemberName(string memberName)
{
_memberNameIndex.TryGetValue(memberName, out var binding);
return binding;
}
/// <summary>清除所有索引</summary>
public void Clear()
{
_root.Children.Clear();
_memberNameIndex.Clear();
}
}
}
@@ -91,6 +91,12 @@ namespace XericUI.ReflectionUIGenerator
bindingCollection.SourceMetaDataSet = this;
var scheduler = new AvrgMvvmScheduler(resolverConfig);
// ★ 构建前缀树索引
var trie = new AvrgBindingTrie();
string typePrefix = businessTarget != null
? businessTarget.GetType().FullName + "."
: "";
foreach (var metaData in _metaDataList)
{
if (metaData.AccessorFactory == null) continue;
@@ -137,9 +143,14 @@ namespace XericUI.ReflectionUIGenerator
string subPath = subMetaData.PropertyPath;
if (string.IsNullOrEmpty(subPath)) continue;
// 创建子属性访问器
var subAccessor = new AvrgSubPropertyAccessor(
accessor, subPath, subMetaData);
var subBinding = new AvrgMvvmBinding
{
MetaData = subMetaData,
Accessor = subAccessor,
ReadFunc = () => ReadSubProperty(accessor.GetValue(), subPath),
WriteAction = (val) =>
{
@@ -157,8 +168,13 @@ namespace XericUI.ReflectionUIGenerator
// 注册到调度器
scheduler.Register(binding);
bindingCollection.Add(binding);
// ★ 插入前缀树(完整路径: Type.FullName.MemberName)
string fullPath = typePrefix + metaData.MemberName;
trie.Insert(fullPath, binding);
}
bindingCollection.SetTrie(trie);
bindingCollection.AttachScheduler(scheduler);
return bindingCollection;
}
@@ -45,21 +45,75 @@ namespace XericUI.ReflectionUIGenerator
/// <summary>数据侧写入的缓存值</summary>
public object DataCachedValue { get; set; }
// ============ 验证状态 ============
private ValidationState _validationState = ValidationState.Normal;
/// <summary>当前验证状态</summary>
public ValidationState CurrentValidationState => _validationState;
/// <summary>验证状态变更事件 — UI 组件订阅以切换样式</summary>
public event Action<ValidationState> OnValidationStateChanged;
/// <summary>
/// 校验值是否满足元数据约束
/// 返回:Normal(通过)、Warning(放行但警告)、Invalid(驳回)
/// </summary>
public ValidationState ValidateValue(object value)
{
if (MetaData == null) return ValidationState.Normal;
// 1. 空值检查(Required)
if (MetaData.Required && (value == null || (value is string s && string.IsNullOrEmpty(s))))
return ValidationState.Invalid;
// 2. 范围检查(RangeMin/RangeMax)
if (value is IConvertible)
{
float num = Convert.ToSingle(value);
if (MetaData.RangeMin.HasValue && num < MetaData.RangeMin.Value)
return ValidationState.Invalid;
if (MetaData.RangeMax.HasValue && num > MetaData.RangeMax.Value)
return ValidationState.Invalid;
}
return ValidationState.Normal;
}
/// <summary>更新验证状态并触发回调(仅状态变化时触发)</summary>
internal void SetValidationState(ValidationState newState)
{
if (_validationState != newState)
{
_validationState = newState;
OnValidationStateChanged?.Invoke(_validationState);
}
}
/// <summary>重置验证状态为 Normal(不触发事件)</summary>
internal void ResetValidationState()
{
_validationState = ValidationState.Normal;
}
// ============ UI 绑定 ============
/// <summary>UI 值变更回调委托 — UI 组件订阅此回调以更新显示</summary>
public Action<object> OnUIDataChanged;
/// <summary>UI 值变更回调委托 — UI 组件订阅此回调以更新显示(附带验证状态)</summary>
public Action<object, ValidationState> OnUIDataChanged;
/// <summary>通知 UI 更新值</summary>
public void NotifyUIUpdate(object value)
/// <summary>通知 UI 更新值(带验证状态)</summary>
public void NotifyUIUpdate(object value, ValidationState state)
{
OnUIDataChanged?.Invoke(value);
OnUIDataChanged?.Invoke(value, state);
}
/// <summary>清除所有 UI 变更订阅</summary>
public void ClearUIEvent()
{
OnUIDataChanged = null;
OnValidationStateChanged = null;
}
// ============ 便捷方法 ============
@@ -4,12 +4,13 @@ using System.Collections.Generic;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 绑定集合 — 统一管理所有 AvrgMvvmBinding
/// 绑定集合 — 统一管理所有 AvrgMvvmBinding,提供字符串索引器和前缀树快速查找
/// </summary>
public class AvrgMvvmBindingCollection : IEnumerable<AvrgMvvmBinding>
{
private List<AvrgMvvmBinding> _bindings = new List<AvrgMvvmBinding>();
private AvrgMvvmScheduler _scheduler;
private AvrgBindingTrie _trie;
/// <summary>
/// 绑定的元数据集引用(可选)
@@ -21,6 +22,11 @@ namespace XericUI.ReflectionUIGenerator
/// </summary>
public int Count => _bindings.Count;
/// <summary>
/// 调度器引用(在 BuildMDVM 时设置)
/// </summary>
public AvrgMvvmScheduler Scheduler => _scheduler;
/// <summary>
/// 添加绑定
/// </summary>
@@ -29,16 +35,53 @@ namespace XericUI.ReflectionUIGenerator
_bindings.Add(binding);
}
/// <summary>
/// 设置前缀树索引(由 BuildMDVM 构建后注入)
/// </summary>
internal void SetTrie(AvrgBindingTrie trie)
{
_trie = trie;
}
/// <summary>
/// 获取指定索引的绑定
/// </summary>
public AvrgMvvmBinding this[int index] => _bindings[index];
/// <summary>
/// 获取指定成员名称的绑定
/// 通过成员名访问绑定(如 bindingCollection["speed"])
/// get: 从数据源读取当前值
/// set: 走完整的 MVVM 管线:WriteFromData → MarkDataDirty → Flush → UI 同步
/// </summary>
public object this[string memberName]
{
get
{
var binding = FindByMemberName(memberName);
return binding?.ReadFromDataSource();
}
set
{
var binding = FindByMemberName(memberName);
if (binding != null && _scheduler != null)
{
binding.WriteFromData(value);
_scheduler.MarkDataDirty(binding);
_scheduler.Flush();
}
}
}
/// <summary>
/// 获取指定成员名称的绑定(优先查 Trie,回退线性查找)
/// </summary>
public AvrgMvvmBinding FindByMemberName(string memberName)
{
if (_trie != null)
{
var found = _trie.Find(memberName) ?? _trie.FindByMemberName(memberName);
if (found != null) return found;
}
return _bindings.Find(b => b.MetaData?.MemberName == memberName);
}
@@ -72,6 +115,7 @@ namespace XericUI.ReflectionUIGenerator
public void Clear()
{
_bindings.Clear();
_trie?.Clear();
_scheduler?.Clear();
}
@@ -9,8 +9,11 @@ namespace XericUI.ReflectionUIGenerator
///
/// 冲突解决策略:
/// - 仅数据脏 → 通知 UI 更新
/// - 仅 UI 脏 → 写入数据源
/// - 仅 UI 脏 → 写入数据源(写入前校验,Invalid 驳回)
/// - 双方都脏 → 走配置的冲突解决策略
///
/// 验证拦截:在 CommitToDataSource 前通过 Binding.ValidateValue() 校验。
/// Invalid 值被驳回不写入,仅通知 UI 进入无效状态。
/// </summary>
public class AvrgMvvmScheduler
{
@@ -79,20 +82,36 @@ namespace XericUI.ReflectionUIGenerator
if (uiDirty && dataDirty)
{
// 双方都脏 — 走冲突解决
// 双方都脏 — 走冲突解决(内部含验证)
ResolveConflict(binding);
}
else if (dataDirty)
{
// 仅数据侧更新 → 通知 UI
// 仅数据侧更新 → 验证后通知 UI
var value = binding.DataCachedValue ?? binding.ReadFromDataSource();
var state = binding.ValidateValue(value);
binding.SetValidationState(state);
binding.CommitToDataSource(value);
binding.NotifyUIUpdate(value);
binding.NotifyUIUpdate(value, state);
}
else if (uiDirty)
{
// 仅 UI 侧更新 → 写入数据源
binding.CommitToDataSource(binding.UICachedValue);
// 仅 UI 侧更新 → 验证后写入数据源
var value = binding.UICachedValue;
var state = binding.ValidateValue(value);
if (state == ValidationState.Invalid)
{
// ★ 无效值:驳回不写入,仅通知 UI 进入无效态
binding.SetValidationState(state);
binding.NotifyUIUpdate(value, state);
return;
}
binding.SetValidationState(state);
binding.CommitToDataSource(value);
// UI 是发起方,不再通知自身(但验证状态变更需通知)
}
}
@@ -106,8 +125,10 @@ namespace XericUI.ReflectionUIGenerator
var resolvedValue = OnConflictDetected.Invoke(binding, DefaultStrategy);
if (resolvedValue != null)
{
var state = binding.ValidateValue(resolvedValue);
binding.SetValidationState(state);
binding.CommitToDataSource(resolvedValue);
binding.NotifyUIUpdate(resolvedValue);
binding.NotifyUIUpdate(resolvedValue, state);
return;
}
}
@@ -120,8 +141,10 @@ namespace XericUI.ReflectionUIGenerator
// 2. 检查 ResolverConfig 中的自定义策略
if (_resolverConfig != null && _resolverConfig.TryResolve(binding, out object configResolvedValue))
{
var state = binding.ValidateValue(configResolvedValue);
binding.SetValidationState(state);
binding.CommitToDataSource(configResolvedValue);
binding.NotifyUIUpdate(configResolvedValue);
binding.NotifyUIUpdate(configResolvedValue, state);
return;
}
@@ -129,38 +152,70 @@ namespace XericUI.ReflectionUIGenerator
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);
var val = binding.UICachedValue;
var state = binding.ValidateValue(val);
if (state == ValidationState.Invalid)
{
binding.SetValidationState(state);
binding.NotifyUIUpdate(val, state);
return;
}
binding.SetValidationState(state);
binding.CommitToDataSource(val);
binding.NotifyUIUpdate(val, state);
break;
}
// 值不同,回退到 PreferUI
binding.CommitToDataSource(binding.UICachedValue);
binding.NotifyUIUpdate(binding.UICachedValue);
Debug.LogWarning($"[AvrgMvvmScheduler] 值冲突(CompareValues回退PreferUI): {binding.MetaData?.MemberName}");
break;
case ConflictResolutionStrategy.PreferData:
{
var dataValue = binding.DataCachedValue ?? binding.ReadFromDataSource();
var state = binding.ValidateValue(dataValue);
binding.SetValidationState(state);
binding.CommitToDataSource(dataValue);
binding.NotifyUIUpdate(dataValue, state);
break;
}
case ConflictResolutionStrategy.CompareValues:
{
object uiVal = binding.UICachedValue;
object dataVal = binding.DataCachedValue ?? binding.ReadFromDataSource();
if (Equals(uiVal, dataVal))
{
// 值相同,忽略冲突
var state2 = binding.ValidateValue(dataVal);
binding.SetValidationState(state2);
binding.NotifyUIUpdate(dataVal, state2);
break;
}
// 值不同,回退到 PreferUI
var val = binding.UICachedValue;
var state = binding.ValidateValue(val);
if (state == ValidationState.Invalid)
{
binding.SetValidationState(state);
binding.NotifyUIUpdate(val, state);
return;
}
binding.SetValidationState(state);
binding.CommitToDataSource(val);
binding.NotifyUIUpdate(val, state);
Debug.LogWarning($"[AvrgMvvmScheduler] 值冲突(CompareValues回退PreferUI): {binding.MetaData?.MemberName}");
break;
}
default:
binding.CommitToDataSource(binding.UICachedValue);
binding.NotifyUIUpdate(binding.UICachedValue);
break;
{
var val = binding.UICachedValue;
var state = binding.ValidateValue(val);
binding.SetValidationState(state);
binding.CommitToDataSource(val);
binding.NotifyUIUpdate(val, state);
break;
}
}
}
@@ -0,0 +1,79 @@
using System;
using System.Reflection;
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 子属性值访问器 — 通过父访问器 + PropertyPath 访问结构的子字段
///
/// 用于拆分属性([XuiavrgSplit])场景,如 Vector3 的 x/y/z 分量。
/// GetValue 通过反射读取父值的子字段/属性;SetValue 由绑定层的 WriteAction 处理。
/// </summary>
internal class AvrgSubPropertyAccessor : IAvrgValueAccessor
{
private readonly IAvrgValueAccessor _parentAccessor;
private readonly string _propertyPath;
private readonly AvrgMemberMetaData _metaData;
public AvrgSubPropertyAccessor(
IAvrgValueAccessor parentAccessor,
string propertyPath,
AvrgMemberMetaData metaData)
{
_parentAccessor = parentAccessor ?? throw new ArgumentNullException(nameof(parentAccessor));
_propertyPath = propertyPath ?? throw new ArgumentNullException(nameof(propertyPath));
_metaData = metaData;
}
public Type ValueType => _metaData?.ValueType ?? typeof(float);
public bool IsReadOnly => _parentAccessor.IsReadOnly;
public object GetValue()
{
var parentValue = _parentAccessor.GetValue();
if (parentValue == null || string.IsNullOrEmpty(_propertyPath))
return null;
var type = parentValue.GetType();
var field = type.GetRuntimeField(_propertyPath);
if (field != null) return field.GetValue(parentValue);
var prop = type.GetRuntimeProperty(_propertyPath);
return prop?.GetValue(parentValue);
}
/// <summary>
/// 子属性设置由绑定层的 WriteAction 统一处理(ReadSubProperty/WriteSubProperty),
/// 此方法不应被直接调用。
/// </summary>
public void SetValue(object value)
{
var parentValue = _parentAccessor.GetValue();
if (parentValue == null) return;
var type = parentValue.GetType();
var field = type.GetRuntimeField(_propertyPath);
if (field != null)
{
field.SetValue(parentValue, value);
_parentAccessor.SetValue(parentValue);
return;
}
var prop = type.GetRuntimeProperty(_propertyPath);
if (prop != null && prop.CanWrite)
{
prop.SetValue(parentValue, value);
_parentAccessor.SetValue(parentValue);
}
}
public string FormatDisplay(object value)
{
if (_metaData?.FormatText != null && value is IFormattable formattable)
return formattable.ToString(_metaData.FormatText, null);
return value?.ToString() ?? "";
}
}
}
@@ -0,0 +1,13 @@
namespace XericUI.ReflectionUIGenerator
{
/// <summary>
/// 值校验状态枚举
/// Normal = 正常(通过校验),Warning = 警告(值在警告范围但放行),Invalid = 无效(值被驳回不写入)
/// </summary>
public enum ValidationState
{
Normal = 0,
Warning = 1,
Invalid = 2
}
}
@@ -308,14 +308,14 @@ namespace XericUI.ReflectionUIGenerator
foreach (var binding in _bindings)
{
var value = binding.ReadFromDataSource();
binding.NotifyUIUpdate(value);
binding.NotifyUIUpdate(value, ValidationState.Normal);
if (binding.SubBindings?.Count > 0)
{
foreach (var subBinding in binding.SubBindings)
{
var subValue = subBinding.ReadFromDataSource();
subBinding.NotifyUIUpdate(subValue);
subBinding.NotifyUIUpdate(subValue, ValidationState.Normal);
}
}
}
@@ -127,16 +127,17 @@ namespace XericUI.ReflectionUIGenerator
if (bindingCollection != null)
{
var matchedBindings = bindingCollection.FindByMemberName(metaData.MemberName);
if (matchedBindings != null)
var scheduler = bindingCollection.Scheduler;
var matchedBinding = bindingCollection.FindByMemberName(metaData.MemberName);
if (matchedBinding != null)
{
entry.AddBinding(matchedBindings, null);
entry.AddBinding(matchedBinding, scheduler);
}
else
{
foreach (var binding in bindingCollection)
{
entry.AddBinding(binding, null);
entry.AddBinding(binding, scheduler);
break;
}
}
@@ -156,13 +157,14 @@ namespace XericUI.ReflectionUIGenerator
if (CurrentEntries != null && bindingCollection != null)
{
var scheduler = bindingCollection.Scheduler;
foreach (var entry in CurrentEntries)
{
if (entry is XuiFieldTempEntry tempEntry)
{
var matched = bindingCollection.FindByMemberName(entry.EntryName);
if (matched != null)
tempEntry.AddBinding(matched, null);
tempEntry.AddBinding(matched, scheduler);
}
}
}
@@ -457,5 +459,7 @@ namespace XericUI.ReflectionUIGenerator
_leafGroupMap.Clear();
}
#endregion
}
}