修复表格在数据删除方面的用法,增加了CheckUpdate函数用于记录操作栈,合并数据调用。

This commit is contained in:
2026-07-29 19:32:46 +08:00
parent c5fa82150f
commit 541053c170
8 changed files with 251 additions and 7 deletions
@@ -194,6 +194,7 @@ namespace XericUI.XTable.Rendering.Component
System.Array.Copy(m_RowHeights, newArr, m_RowHeights.Length);
newArr[newLen - 1] = height;
m_RowHeights = newArr;
RecordStructuralChange(false);
MarkDirty(TableDirtyType.LayoutChanged);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
@@ -212,6 +213,7 @@ namespace XericUI.XTable.Rendering.Component
System.Array.Copy(m_ColWidths, newArr, m_ColWidths.Length);
newArr[newLen - 1] = width;
m_ColWidths = newArr;
RecordStructuralChange(false);
MarkDirty(TableDirtyType.LayoutChanged);
#if UNITY_EDITOR
UnityEditor.EditorUtility.SetDirty(this);
@@ -256,6 +258,8 @@ namespace XericUI.XTable.Rendering.Component
UnityEditor.Undo.RecordObject(this, "Remove Table Row");
#endif
int colCount = m_ColWidths.Length;
CaptureActivePrefabReleaseCallbacks();
RecordStructuralChange(true);
// 先解除涉及该行的合并
for (int c = 0; c < colCount; c++)
@@ -303,6 +307,8 @@ namespace XericUI.XTable.Rendering.Component
UnityEditor.Undo.RecordObject(this, "Remove Table Column");
#endif
int rowCount = m_RowHeights.Length;
CaptureActivePrefabReleaseCallbacks();
RecordStructuralChange(true);
// 先解除涉及该列的合并
for (int r = 0; r < rowCount; r++)
@@ -410,7 +416,8 @@ namespace XericUI.XTable.Rendering.Component
private void OnHandleSelectionChanged(XTableSelectionHandle handle)
{
MarkDirty(TableDirtyType.SelectionChanged);
RefreshSelectionOverlays();
if (!IsCheckingUpdate)
RefreshSelectionOverlays();
}
/// <summary>裁剪选区到当前行列尺寸范围内</summary>
@@ -562,6 +569,7 @@ namespace XericUI.XTable.Rendering.Component
/// </summary>
public void ClearTable()
{
ThrowIfCheckingUpdate(nameof(ClearTable));
m_IsRebuilding = true;
try
{
@@ -624,6 +632,8 @@ namespace XericUI.XTable.Rendering.Component
public void ApplyClipboardData(XTableClipboardData data)
{
if (data == null) return;
CaptureActivePrefabReleaseCallbacks();
RecordStructuralChange(true);
// 确保数据层存在
if (m_TableData == null)
@@ -640,9 +650,8 @@ namespace XericUI.XTable.Rendering.Component
// 写回单元格文本与合并信息
data.PopulateToTable(this);
MarkDirty(TableDirtyType.DataChanged);
RecalculateTotalSize();
RefreshView();
RequestRefresh(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged, true);
SetChildrenVisible(m_ChildrenVisible);
}
}
@@ -39,7 +39,7 @@ namespace XericUI.XTable.Rendering.Component
return;
}
if (m_IsRebuilding || !m_DirtyFlag.IsDirty) return;
if (m_IsRebuilding || IsCheckingUpdate || !m_DirtyFlag.IsDirty) return;
var flags = m_DirtyFlag.Flags;
if ((flags & ~(TableDirtyType.ScrollChanged | TableDirtyType.SelectionChanged)) == 0)
@@ -262,9 +262,10 @@ namespace XericUI.XTable.Rendering.Component
}
// 标记脏并刷新
MarkDirty(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged);
CaptureActivePrefabReleaseCallbacks();
RecordStructuralChange(true);
RecalculateTotalSize();
RefreshView();
RequestRefresh(TableDirtyType.DataChanged | TableDirtyType.LayoutChanged, true);
}
#endregion
@@ -225,6 +225,37 @@ namespace XericUI.XTable.Rendering.Component
#region 完整刷新
private void CommitStructuralRenderCleanup(bool invalidatePrefabBindings,
Dictionary<GameObject, System.Action<GameObject>> prefabReleaseCallbacks)
{
ReleaseAllTrackedCells();
ReleaseFrozenCells();
m_LastRenderedCells.Clear();
if (m_ActivePrefabMap != null)
{
foreach (var pair in m_ActivePrefabMap)
{
if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.ContainsKey(pair.Value)) continue;
var data = TableData.GetCell(pair.Key.Item1, pair.Key.Item2);
if (data?.OnPrefabRelease != null && prefabReleaseCallbacks != null
&& !prefabReleaseCallbacks.ContainsKey(pair.Value))
prefabReleaseCallbacks.Add(pair.Value, data.OnPrefabRelease);
}
m_ActivePrefabMap.Clear();
}
if (invalidatePrefabBindings)
m_PrefabPool?.ReleaseAll(instance =>
{
if (prefabReleaseCallbacks != null && prefabReleaseCallbacks.TryGetValue(instance, out var callback))
callback(instance);
});
m_QuadtreeDirty = true;
m_StyleNamespaceDirty = true;
}
public void RefreshView()
{
if (m_Assembler == null)
@@ -300,6 +331,7 @@ namespace XericUI.XTable.Rendering.Component
private void RefreshViewLight()
{
TableDirtyType refreshedFlags = m_DirtyFlag.Flags;
if (m_Assembler == null)
{
if (rectTransform != null)
@@ -0,0 +1,164 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace XericUI.XTable.Rendering.Component
{
public partial class XericUIActionTable
{
private sealed class UpdateContext
{
public TableDirtyType DirtyFlags;
public bool HasStructuralChange;
public bool InvalidatePrefabBindings;
public bool RequiresImmediateRefresh;
public readonly Dictionary<GameObject, Action<GameObject>> PrefabReleaseCallbacks = new();
public void Reset()
{
DirtyFlags = TableDirtyType.None;
HasStructuralChange = false;
InvalidatePrefabBindings = false;
RequiresImmediateRefresh = false;
PrefabReleaseCallbacks.Clear();
}
public void MergeFrom(UpdateContext child)
{
DirtyFlags |= child.DirtyFlags;
HasStructuralChange |= child.HasStructuralChange;
InvalidatePrefabBindings |= child.InvalidatePrefabBindings;
RequiresImmediateRefresh |= child.RequiresImmediateRefresh;
foreach (var pair in child.PrefabReleaseCallbacks)
if (!PrefabReleaseCallbacks.ContainsKey(pair.Key))
PrefabReleaseCallbacks.Add(pair.Key, pair.Value);
}
}
public sealed class UpdateScope : IDisposable
{
private XericUIActionTable m_Table;
private readonly long m_Token;
private bool m_Disposed;
internal UpdateScope(XericUIActionTable table, long token)
{
m_Table = table;
m_Token = token;
}
public void Dispose()
{
if (m_Disposed) return;
m_Disposed = true;
if (m_Table != null)
m_Table.EndCheckUpdate(m_Token);
m_Table = null;
}
}
[NonSerialized] private readonly Stack<UpdateContext> m_UpdateContextStack = new();
[NonSerialized] private readonly Stack<UpdateContext> m_UpdateContextPool = new();
[NonSerialized] private long m_NextUpdateToken;
[NonSerialized] private readonly Stack<long> m_UpdateTokenStack = new();
private bool IsCheckingUpdate => m_UpdateContextStack.Count > 0;
/// <summary>
/// 开始一个可嵌套的批量更新作用域。作用域内的数据修改立即生效,渲染与结构资源整理延迟到最外层 Dispose。
/// </summary>
public UpdateScope CheckUpdate()
{
var context = m_UpdateContextPool.Count > 0 ? m_UpdateContextPool.Pop() : new UpdateContext();
context.Reset();
m_UpdateContextStack.Push(context);
long token = ++m_NextUpdateToken;
m_UpdateTokenStack.Push(token);
return new UpdateScope(this, token);
}
private void EndCheckUpdate(long token)
{
if (m_UpdateTokenStack.Count == 0 || m_UpdateTokenStack.Peek() != token)
{
Debug.LogError("[XericUIActionTable] CheckUpdate 作用域必须按 LIFO 顺序释放。", this);
return;
}
m_UpdateTokenStack.Pop();
UpdateContext context = m_UpdateContextStack.Pop();
if (m_UpdateContextStack.Count > 0)
{
m_UpdateContextStack.Peek().MergeFrom(context);
ReturnUpdateContext(context);
return;
}
try
{
CommitUpdateContext(context);
}
finally
{
ReturnUpdateContext(context);
}
}
private void ReturnUpdateContext(UpdateContext context)
{
context.Reset();
m_UpdateContextPool.Push(context);
}
private void CommitUpdateContext(UpdateContext context)
{
if (context.DirtyFlags == TableDirtyType.None) return;
if (context.HasStructuralChange)
CommitStructuralRenderCleanup(context.InvalidatePrefabBindings, context.PrefabReleaseCallbacks);
ApplyDirty(context.DirtyFlags);
if (context.RequiresImmediateRefresh)
RefreshView();
RefreshSelectionOverlays();
}
private void RecordStructuralChange(bool invalidatePrefabBindings)
{
if (!IsCheckingUpdate) return;
UpdateContext context = m_UpdateContextStack.Peek();
context.HasStructuralChange = true;
context.InvalidatePrefabBindings |= invalidatePrefabBindings;
}
private void CaptureActivePrefabReleaseCallbacks()
{
if (!IsCheckingUpdate || m_ActivePrefabMap == null) return;
var callbacks = m_UpdateContextStack.Peek().PrefabReleaseCallbacks;
foreach (var pair in m_ActivePrefabMap)
{
var callback = TableData.GetCell(pair.Key.Item1, pair.Key.Item2)?.OnPrefabRelease;
if (callback != null && !callbacks.ContainsKey(pair.Value))
callbacks.Add(pair.Value, callback);
}
}
private void RequestRefresh(TableDirtyType type, bool immediate)
{
MarkDirty(type);
if (IsCheckingUpdate)
{
m_UpdateContextStack.Peek().RequiresImmediateRefresh |= immediate;
return;
}
if (immediate)
RefreshView();
}
private void ThrowIfCheckingUpdate(string operation)
{
if (IsCheckingUpdate)
throw new InvalidOperationException($"{operation} 不能在 CheckUpdate 作用域内调用。");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 28fb43aa1d17ba14b9fa4cf1bd2ad055
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -347,7 +347,7 @@ namespace XericUI.XTable.Rendering.Component
private void Update()
{
UpdateSelectionAutoScroll();
if (m_IsRebuilding || !m_DirtyFlag.IsDirty) return;
if (m_IsRebuilding || IsCheckingUpdate || !m_DirtyFlag.IsDirty) return;
var flags = m_DirtyFlag.Flags;
// 仅滚动或选区变更 → 轻刷新(不重建边框/背景/预制体全回收)
@@ -385,6 +385,16 @@ namespace XericUI.XTable.Rendering.Component
#region 脏标记与滚动回调
public void MarkDirty(TableDirtyType type)
{
if (IsCheckingUpdate)
{
m_UpdateContextStack.Peek().DirtyFlags |= type;
return;
}
ApplyDirty(type);
}
private void ApplyDirty(TableDirtyType type)
{
m_DirtyFlag.Mark(type);
@@ -402,6 +412,7 @@ namespace XericUI.XTable.Rendering.Component
private void OnScrollValueChanged(Vector2 _)
{
if (IsCheckingUpdate) return;
// 防重入:RefreshViewLight 中操作 content 可能触发 ScrollRect 再次回调
if (m_IsRefreshingFromScroll) return;
m_IsRefreshingFromScroll = true;
@@ -147,6 +147,22 @@ namespace XericUI.XTable.Rendering.Elements
KillObject(instance);
}
/// <summary>销毁所有按 cellId 绑定的实例,并在销毁前通知调用方。</summary>
public void ReleaseAll(System.Action<GameObject> beforeRelease = null)
{
foreach (var pool in m_Pools.Values)
{
foreach (var instance in pool.Slots.Values)
{
beforeRelease?.Invoke(instance);
KillObject(instance);
}
pool.Slots.Clear();
}
m_Pools.Clear();
m_TotalCreated = 0;
}
/// <summary>结束当前渲染周期。实例保留在全局槽位中,以便跨区域复用。</summary>
public void ReturnAll()
{