perf: BuildNode 延迟 CreatePathId + DisplayPath 同名去重修复

- BuildNode 不再调 FromModelItem/CreatePathId,改用父节点 DisplayPath 拼接
- CreatePathId 只在保存别名时通过 EnsureFullNodeKey 触发
- LazyLoadChildren 用 nameCount 字典计算 siblingIndex,BuildNode 追加 #N 去重
- 子节点 DisplayPath 拼接时剥离父路径中的 ||前缀和#N后缀,与 FromModelItem 一致
- LazyLoadChildren 直接用 parentNode.ModelItem,不再调 FindModelItemByNodeKey
- 别名匹配去掉 fallback,精确到#N
- _dataStore.LoadAll() 提升到循环外
This commit is contained in:
tian 2026-06-01 10:21:02 +08:00
parent 61318b450d
commit d670ae5c3c

View File

@ -279,8 +279,12 @@ namespace NavisworksTransport.UI.WPF.Views
// 全量加载别名到内存
Dictionary<string, string> aliasMap = null;
Dictionary<string, string> displayPathToAlias = null;
if (_dataStore != null)
{
aliasMap = _dataStore.LoadAll();
displayPathToAlias = BuildDisplayPathAliasMap(aliasMap);
}
// 遍历所有 root items
foreach (var model in doc.Models)
@ -288,7 +292,7 @@ namespace NavisworksTransport.UI.WPF.Views
var rootItem = model.RootItem;
if (rootItem != null)
{
var node = BuildNode(rootItem, aliasMap, 0);
var node = BuildNode(rootItem, aliasMap, displayPathToAlias, 0, "", 0);
if (node != null)
rootNodes.Add(node);
}
@ -310,45 +314,51 @@ namespace NavisworksTransport.UI.WPF.Views
}
/// <summary>
/// 递归构建节点树(主线程调用)
/// 递归构建节点树(主线程调用)。
/// siblingIndex 用于同名兄弟去重0 表示第一个(不追加后缀)。
/// </summary>
private AliasNodeViewModel BuildNode(Autodesk.Navisworks.Api.ModelItem modelItem,
Dictionary<string, string> aliasMap, int depth)
Dictionary<string, string> aliasMap,
Dictionary<string, string> displayPathToAlias,
int depth, string parentDisplayPath, int siblingIndex)
{
if (modelItem == null) return null;
AliasNodeIdentity identity;
try
string displayName = modelItem.DisplayName ?? "?";
// 父节点 key 可能是完整格式 "IndexPath||DisplayPath",也可能是 ".../name#N"
// 只取纯 DisplayPath 部分(去掉 || 前缀和结尾的 #N 后缀)
string parentPath = parentDisplayPath;
int sepIdx = parentPath.IndexOf("||", StringComparison.Ordinal);
if (sepIdx >= 0) parentPath = parentPath.Substring(sepIdx + 2);
// 去掉结尾的 #数字 后缀(中间层不加 siblingIndex
int hashIdx = parentPath.LastIndexOf('#');
if (hashIdx > 0)
{
identity = AliasNodeIdentity.FromModelItem(modelItem);
string suffix = parentPath.Substring(hashIdx + 1);
bool isNumeric = suffix.Length > 0 && suffix.All(char.IsDigit);
if (isNumeric) parentPath = parentPath.Substring(0, hashIdx);
}
catch (Exception ex)
{
LogManager.Error($"[别名树] BuildNode FromModelItem 异常: {ex.Message}");
return null;
}
string key = identity.ToKey();
string displayPath = string.IsNullOrEmpty(parentPath)
? displayName
: parentPath + "/" + displayName;
// 同名兄弟去重:第二个起追加 #索引
string displayPathKey = siblingIndex > 0 ? displayPath + "#" + siblingIndex : displayPath;
// 别名匹配:精确匹配 displayPathKey
string alias = null;
aliasMap?.TryGetValue(key, out alias);
// 旧格式兼容:也尝试用纯 DisplayPath 查找
if (alias == null && !string.IsNullOrEmpty(identity.IndexPath))
{
aliasMap?.TryGetValue(identity.DisplayPath, out alias);
}
displayPathToAlias?.TryGetValue(displayPathKey, out alias);
var node = new AliasNodeViewModel
{
NodeKey = key,
NodeKey = displayPathKey,
ModelItem = modelItem,
OriginalName = modelItem.DisplayName ?? "?",
OriginalName = displayName,
Alias = alias,
ChildCount = modelItem.Children.Count(),
Depth = depth
};
_nodeMap[key] = node;
_nodeMap[displayPathKey] = node;
// 深度限制:超过阈值且无别名 → 不展开子节点,不加占位
bool hasChildren = node.ChildCount > 0;
@ -367,10 +377,8 @@ namespace NavisworksTransport.UI.WPF.Views
return node;
}
/// <summary>
/// 懒加载子节点(在 Expanded 事件中触发)
/// </summary>
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> aliasMap)
private void LazyLoadChildren(AliasNodeViewModel parentNode, Dictionary<string, string> aliasMap,
Dictionary<string, string> displayPathToAlias)
{
// 检查是否只有占位节点
if (parentNode.Children.Count != 1 || parentNode.Children[0].OriginalName != "加载中...")
@ -380,16 +388,20 @@ namespace NavisworksTransport.UI.WPF.Views
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null) return;
// 从 nodeKey 反推出 ModelItem
var item = FindModelItemByNodeKey(parentNode.NodeKey);
var item = parentNode.ModelItem;
if (item == null) return;
// 同名去重:统计每个名字已出现的次数
var nameCount = new Dictionary<string, int>();
foreach (var childItem in item.Children)
{
var childNode = BuildNode(childItem, aliasMap, parentNode.Depth + 1);
string name = childItem.DisplayName ?? "?";
nameCount.TryGetValue(name, out int count);
int siblingIdx = count;
nameCount[name] = count + 1;
var childNode = BuildNode(childItem, aliasMap, displayPathToAlias,
parentNode.Depth + 1, parentNode.NodeKey, siblingIdx);
if (childNode != null)
{
childNode.Parent = parentNode;
@ -404,37 +416,84 @@ namespace NavisworksTransport.UI.WPF.Views
}
// ============================================================
// 从 NodeKey 回溯定位 ModelItem仅用 IndexPath
/// <summary>
/// 从 aliasMap 构建 DisplayPath → alias 索引(去掉 IndexPath|| 前缀)
/// </summary>
private static Dictionary<string, string> BuildDisplayPathAliasMap(Dictionary<string, string> aliasMap)
{
if (aliasMap == null || aliasMap.Count == 0) return null;
var map = new Dictionary<string, string>();
foreach (var kvp in aliasMap)
{
// key 格式: "IndexPath||DisplayPath" → 取 DisplayPath 部分
int sepIdx = kvp.Key.IndexOf("||", StringComparison.Ordinal);
string displayPath = sepIdx >= 0 ? kvp.Key.Substring(sepIdx + 2) : kvp.Key;
map[displayPath] = kvp.Value;
}
return map;
}
// ============================================================
/// <summary>
/// 确保节点有完整 NodeKey含 IndexPath。首次保存别名时调用 CreatePathId 升级 key。
/// </summary>
private void EnsureFullNodeKey(AliasNodeViewModel node)
{
if (node == null || node.ModelItem == null) return;
// 已是完整格式
if (node.NodeKey != null && node.NodeKey.Contains("||")) return;
try
{
var identity = AliasNodeIdentity.FromModelItem(node.ModelItem);
string newKey = identity.ToKey();
_nodeMap.Remove(node.NodeKey);
_nodeMap[newKey] = node;
node.NodeKey = newKey;
}
catch (Exception ex)
{
LogManager.Error($"[别名树] EnsureFullNodeKey 失败: {ex.Message}");
}
}
private Autodesk.Navisworks.Api.ModelItem FindModelItemByNodeKey(string key)
{
if (!AliasNodeIdentity.TryParseKey(key, out var identity))
return null;
if (string.IsNullOrEmpty(identity.IndexPath))
return null;
if (string.IsNullOrEmpty(key)) return null;
// 判断 key 格式:含 "||" 为完整格式 → ResolvePathId否则为 DisplayPath → _nodeMap 查找
int sepIdx = key.IndexOf("||", StringComparison.Ordinal);
if (sepIdx >= 0)
{
if (!AliasNodeIdentity.TryParseKey(key, out var identity) || string.IsNullOrEmpty(identity.IndexPath))
return null;
return ResolveByIndexPath(identity.IndexPath);
}
// 简化格式DisplayPath → 查 _nodeMap
if (_nodeMap.TryGetValue(key, out var node) && node.ModelItem != null)
return node.ModelItem;
return null;
}
private Autodesk.Navisworks.Api.ModelItem ResolveByIndexPath(string indexPath)
{
try
{
var doc = NavisApplication.ActiveDocument;
if (doc == null || doc.Models == null) return null;
// 解析 IndexPath: "modelIndex:PathId"
int colonIdx = identity.IndexPath.IndexOf(':');
int colonIdx = indexPath.IndexOf(':');
if (colonIdx < 0) return null;
if (!int.TryParse(identity.IndexPath.Substring(0, colonIdx), out int modelIndex))
if (!int.TryParse(indexPath.Substring(0, colonIdx), out int modelIndex))
return null;
string pathId = identity.IndexPath.Substring(colonIdx + 1);
string pathId = indexPath.Substring(colonIdx + 1);
LogManager.Info($"[别名树] FindByKey: IndexPath={identity.IndexPath}, modelIndex={modelIndex}, pathId={pathId}");
var result = doc.Models.ResolvePathId(new ModelItemPathId { ModelIndex = modelIndex, PathId = pathId });
LogManager.Info($"[别名树] FindByKey: ResolvePathId -> {(result != null ? result.DisplayName : "null")}");
return result;
return doc.Models.ResolvePathId(new ModelItemPathId { ModelIndex = modelIndex, PathId = pathId });
}
catch (Exception ex)
catch
{
LogManager.Error($"[别名树] FindByKey 失败: {key}, {ex.Message}");
return null;
}
}
@ -499,9 +558,10 @@ namespace NavisworksTransport.UI.WPF.Views
// 加载子节点(有占位 → 全量懒加载;否则只找目标那一个)
var aliasMap = _dataStore?.LoadAll();
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
{
LazyLoadChildren(treeNode, aliasMap);
LazyLoadChildren(treeNode, aliasMap, displayAlias);
}
// 在已加载的子节点中查找匹配
@ -518,7 +578,8 @@ namespace NavisworksTransport.UI.WPF.Views
// 没找到(深度超限节点)→ 直接用 ModelItem 创建
if (child == null)
{
child = BuildNode(targetModelItem, aliasMap, treeNode.Depth + 1);
child = BuildNode(targetModelItem, aliasMap, displayAlias,
treeNode.Depth + 1, treeNode.NodeKey, 0);
if (child != null)
{
child.Parent = treeNode;
@ -696,6 +757,8 @@ namespace NavisworksTransport.UI.WPF.Views
if (chain.Count == 1) return true;
// 3. 逐层加载
var aliasMap = _dataStore?.LoadAll();
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
for (int i = 1; i < chain.Count; i++)
{
var targetModelItem = chain[i];
@ -705,7 +768,7 @@ namespace NavisworksTransport.UI.WPF.Views
if (treeNode.Children.Count == 1 && treeNode.Children[0].OriginalName == "加载中...")
{
LogManager.Info($"[别名树] EnsurePathLoaded: L{i} 懒加载子节点");
LazyLoadChildren(treeNode, _dataStore?.LoadAll());
LazyLoadChildren(treeNode, aliasMap, displayAlias);
}
AliasNodeViewModel child = null;
@ -721,7 +784,8 @@ namespace NavisworksTransport.UI.WPF.Views
if (child == null)
{
LogManager.Info($"[别名树] EnsurePathLoaded: L{i} 未匹配, BuildNode 创建");
child = BuildNode(targetModelItem, _dataStore?.LoadAll(), treeNode.Depth + 1);
child = BuildNode(targetModelItem, aliasMap, displayAlias,
treeNode.Depth + 1, treeNode.NodeKey, 0);
if (child != null)
{
child.Parent = treeNode;
@ -974,6 +1038,7 @@ namespace NavisworksTransport.UI.WPF.Views
LogManager.Info($"[别名树] 内联编辑: {newAlias}, NodeKey={node.NodeKey}");
try
{
EnsureFullNodeKey(node);
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.SetAlias(identity, newAlias);
}
@ -1066,6 +1131,7 @@ namespace NavisworksTransport.UI.WPF.Views
var node = GetSelectedNode();
if (node == null || _dataStore == null) return;
EnsureFullNodeKey(node);
AliasNodeIdentity.TryParseKey(node.NodeKey, out var identity);
_dataStore.DeleteAlias(identity);
node.Alias = null;
@ -1183,7 +1249,8 @@ namespace NavisworksTransport.UI.WPF.Views
if (!(e.OriginalSource is TreeViewItem tvi) || !(tvi.DataContext is AliasNodeViewModel node))
return;
var aliasMap = _dataStore?.LoadAll();
LazyLoadChildren(node, aliasMap);
var displayAlias = BuildDisplayPathAliasMap(aliasMap);
LazyLoadChildren(node, aliasMap, displayAlias);
}
private bool _isProgrammaticSelect;