修改经常崩溃的问题,最大原因是并行路径计算(步骤中调用了NW的API)

This commit is contained in:
tian 2025-09-09 13:16:28 +08:00
parent d046e31d6c
commit b449cf08ad
9 changed files with 259 additions and 172 deletions

View File

@ -225,6 +225,9 @@
<Compile Include="src\UI\WPF\Collections\ThreadSafeObservableCollection.cs" />
<Compile Include="src\UI\WPF\Collections\VirtualizedObservableCollection.cs" />
<!-- UI - WPF Interfaces -->
<Compile Include="src\UI\WPF\Interfaces\IPropertyChangeNotifier.cs" />
<!-- UI - WPF Services -->
<Compile Include="src\UI\WPF\Services\BindingExpressionOptimizer.cs" />
<Compile Include="src\UI\WPF\Services\CrossViewModelSynchronizer.cs" />

View File

@ -681,39 +681,17 @@ namespace NavisworksTransport
// 1. 停用ToolPlugin并清理事件订阅
DeactivateToolPlugin();
// 2. 额外的事件订阅清理 - 确保所有可能的重复订阅都被清除
LogManager.WriteLog("[事件清理] 执行额外的事件订阅清理");
// 2. 简化的事件订阅清理 - 移除危险的反射操作
LogManager.WriteLog("[事件清理] 执行安全的事件订阅清理");
try
{
// 移除所有可能的OnToolPluginMouseClicked订阅
var mouseClickedEvent = typeof(PathClickToolPlugin).GetEvent("MouseClicked");
if (mouseClickedEvent != null)
{
var eventField = typeof(PathClickToolPlugin).GetField("MouseClicked",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
if (eventField != null)
{
var currentDelegate = (MulticastDelegate)eventField.GetValue(null);
if (currentDelegate != null)
{
var invocationList = currentDelegate.GetInvocationList();
LogManager.WriteLog($"[事件清理] 发现 {invocationList.Length} 个事件订阅者");
foreach (var handler in invocationList)
{
if (handler.Target == this && handler.Method.Name == "OnToolPluginMouseClicked")
{
PathClickToolPlugin.MouseClicked -= OnToolPluginMouseClicked;
LogManager.WriteLog($"[事件清理] 移除了PathPlanningManager.OnToolPluginMouseClicked订阅");
}
}
}
}
}
// 安全地移除事件订阅,多次取消订阅同一处理程序是安全的
PathClickToolPlugin.MouseClicked -= OnToolPluginMouseClicked;
LogManager.WriteLog("[事件清理] 已安全移除PathPlanningManager.OnToolPluginMouseClicked订阅");
}
catch (Exception cleanupEx)
{
LogManager.WriteLog($"[事件清理] 额外清理过程异常: {cleanupEx.Message}");
LogManager.WriteLog($"[事件清理] 事件清理过程异常: {cleanupEx.Message}");
}
// 注意移除了PathEditState设置StopClickTool只管理工具插件状态不修改业务逻辑状态
@ -2702,6 +2680,47 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 强制重新初始化ToolPlugin公共接口避免反射调用
/// </summary>
/// <param name="subscribeToEvents">是否订阅事件</param>
/// <returns>初始化是否成功</returns>
public bool ForceReinitializeToolPlugin(bool subscribeToEvents = false)
{
try
{
LogManager.WriteLog($"[工具插件重初始化] 开始强制重新初始化ToolPlugin订阅事件: {subscribeToEvents}");
// 1. 停用当前ToolPlugin
bool deactivated = DeactivateToolPlugin();
LogManager.WriteLog($"[工具插件重初始化] 停用结果: {deactivated}");
// 2. 强制重置激活状态
_isToolPluginActive = false;
LogManager.WriteLog("[工具插件重初始化] 已重置激活状态标志");
// 3. 重新激活ToolPlugin
bool activated = ActivateToolPlugin(subscribeToEvents);
LogManager.WriteLog($"[工具插件重初始化] 激活结果(事件订阅: {subscribeToEvents}: {activated}");
if (activated)
{
LogManager.WriteLog("[工具插件重初始化] ToolPlugin重新初始化成功已获得鼠标焦点");
}
else
{
LogManager.WriteLog("[工具插件重初始化] ToolPlugin激活失败");
}
return activated;
}
catch (Exception ex)
{
LogManager.Error($"[工具插件重初始化] 重新初始化ToolPlugin失败: {ex.Message}", ex);
return false;
}
}
#endregion
#region
@ -3157,6 +3176,14 @@ namespace NavisworksTransport
}
}
/// <summary>
/// 检查是否有任何网格可视化已启用
/// </summary>
public bool IsAnyGridVisualizationEnabled
{
get { return _showWalkableGrid || _showObstacleGrid || _showUnknownGrid; }
}
/// <summary>
/// 刷新网格可视化(根据当前设置重新显示)
/// </summary>

View File

@ -436,25 +436,56 @@ namespace NavisworksTransport
/// <param name="excludedPathIds">要保留的路径ID列表</param>
public void ClearPathsExcept(params string[] excludedPathIds)
{
lock (_lockObject)
try
{
var excludedSet = new HashSet<string>(excludedPathIds);
var toRemove = _pathVisualizations.Keys.Where(id => !excludedSet.Contains(id)).ToList();
int removedCount = 0;
foreach (var pathId in toRemove)
lock (_lockObject)
{
if (_pathVisualizations.Remove(pathId))
// 防御性编程:处理空参数
if (excludedPathIds == null)
{
removedCount++;
excludedPathIds = new string[0];
}
var excludedSet = new HashSet<string>(excludedPathIds.Where(id => !string.IsNullOrEmpty(id)));
var toRemove = _pathVisualizations.Keys.Where(id => !excludedSet.Contains(id)).ToList();
// 记录当前状态
LogManager.WriteLog($"[选择性清空] 当前路径数: {_pathVisualizations.Count}, 排除路径数: {excludedSet.Count}, 待清理路径数: {toRemove.Count}");
// 检查排除的路径是否实际存在
var existingExcluded = excludedSet.Where(id => _pathVisualizations.ContainsKey(id)).ToList();
var nonExistingExcluded = excludedSet.Where(id => !_pathVisualizations.ContainsKey(id)).ToList();
if (nonExistingExcluded.Any())
{
LogManager.WriteLog($"[选择性清空] 注意:尝试保留的路径不存在: {string.Join(", ", nonExistingExcluded)}");
}
if (existingExcluded.Any())
{
LogManager.WriteLog($"[选择性清空] 将保留的现有路径: {string.Join(", ", existingExcluded)}");
}
int removedCount = 0;
foreach (var pathId in toRemove)
{
if (_pathVisualizations.Remove(pathId))
{
removedCount++;
}
}
LogManager.WriteLog($"[选择性清空] 成功清空{removedCount}个路径,保留{_pathVisualizations.Count}个路径");
if (removedCount > 0)
{
RequestViewRefresh();
}
}
LogManager.WriteLog($"[选择性清空] 清空{removedCount}个路径,保留{excludedSet.Count}个路径");
if (removedCount > 0)
{
RequestViewRefresh();
}
}
catch (Exception ex)
{
LogManager.Error($"[选择性清空] 清理路径时发生异常: {ex.Message}", ex);
// 即使发生异常也不抛出,避免影响主流程
}
}

View File

@ -2,6 +2,8 @@ using System;
using System.ComponentModel;
using System.Threading;
using System.Threading.Tasks;
using NavisworksTransport.UI.WPF.Interfaces;
using NavisworksTransport.Utils;
namespace NavisworksTransport.Core.UIUpdate.Updates
{
@ -89,28 +91,30 @@ namespace NavisworksTransport.Core.UIUpdate.Updates
await uiStateManager.ExecuteUIUpdateAsync(() =>
{
// 通过反射调用OnPropertyChanged方法如果ViewModel是ViewModelBase的子类
var onPropertyChangedMethod = ViewModel.GetType().GetMethod("OnPropertyChanged",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Protected);
if (onPropertyChangedMethod != null)
// 优先使用IPropertyChangeNotifier接口安全方式
if (ViewModel is IPropertyChangeNotifier notifier)
{
onPropertyChangedMethod.Invoke(ViewModel, new object[] { PropertyName });
notifier.NotifyPropertyChanged(PropertyName);
}
else
{
// 如果没有找到OnPropertyChanged方法尝试直接触发PropertyChanged事件
var eventField = ViewModel.GetType().GetField("PropertyChanged",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (eventField != null)
// 如果ViewModel不实现IPropertyChangeNotifier直接触发PropertyChanged事件
// 这是最后的备用方案,避免使用反射
try
{
var eventDelegate = eventField.GetValue(ViewModel) as PropertyChangedEventHandler;
eventDelegate?.Invoke(ViewModel, new PropertyChangedEventArgs(PropertyName));
// 创建PropertyChangedEventArgs并尝试通过事件触发
var eventArgs = new PropertyChangedEventArgs(PropertyName);
// 如果ViewModel有公共的PropertyChanged事件我们可以通过显式访问来触发
// 但是由于事件的限制,我们无法从外部直接调用事件
// 这种情况下记录警告提醒开发者ViewModel应该实现IPropertyChangeNotifier
LogManager.Warning($"ViewModel ({ViewModel.GetType().Name}) 未实现IPropertyChangeNotifier接口无法安全触发属性变更通知");
throw new InvalidOperationException($"ViewModel必须实现IPropertyChangeNotifier接口才能使用ViewModelRefreshOperation");
}
else
catch (Exception ex)
{
throw new InvalidOperationException("无法找到触发PropertyChanged事件的方法");
LogManager.Error($"触发PropertyChanged事件失败: {ex.Message}");
throw new InvalidOperationException("无法找到触发PropertyChanged事件的方法请确保ViewModel实现IPropertyChangeNotifier接口");
}
}
}, context.TimeoutMilliseconds);

View File

@ -1120,103 +1120,105 @@ namespace NavisworksTransport.PathPlanning
LogManager.Info($"[包围盒障碍物处理] 输入统计 - 总模型项: {totalItems}, 将排除通道元素: {channelItemsSet.Count}");
// 临时修改:注释掉并行处理以避免线程安全问题
// 使用并行处理提高性能使用50%的CPU内核以平衡性能和稳定性
var lockObject = new object();
Parallel.ForEach(allItems, new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) }, item =>
// var lockObject = new object();
// Parallel.ForEach(allItems, new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, Environment.ProcessorCount / 2) }, item =>
// 改为单线程处理
foreach (var item in allItems)
{
try
{
Interlocked.Increment(ref processedItems);
processedItems++; // 改为普通递增
// 1. 排除通道项(包括容器节点和子节点)
if (channelItemsSet.Contains(item) || IsChildOfChannelItems(item, channelItemsSet))
{
Interlocked.Increment(ref channelExcludedItems);
return;
channelExcludedItems++; // 改为普通递增
continue; // 改为continue
}
// 2. 检查是否有几何体,只对有几何体的元素进行处理
if (!item.HasGeometry)
{
Interlocked.Increment(ref noGeometryItems);
return;
noGeometryItems++; // 改为普通递增
continue; // 改为continue
}
// 2.5 检查是否是容器节点(有子节点的元素通常是组/层级)
// 容器节点的包围盒会包含所有子元素,不应作为障碍物
if (item.Children.Any())
{
Interlocked.Increment(ref containerItems);
return; // 跳过容器节点
containerItems++; // 改为普通递增
continue; // 跳过容器节点改为continue
}
// 3. 获取包围盒
var bbox = item.BoundingBox();
if (bbox == null)
{
Interlocked.Increment(ref noBoundsItems);
return;
noBoundsItems++; // 改为普通递增
continue; // 改为continue
}
// 4. 检查高度范围(重要优化:提前过滤不相关的模型项)
if (!IsInScanHeightRange(bbox, gridMap, scanHeight))
{
Interlocked.Increment(ref outOfHeightRangeItems);
return;
outOfHeightRangeItems++; // 改为普通递增
continue; // 改为continue
}
// 5. 计算包围盒覆盖的网格单元
var coveredCells = CalculateBoundingBoxGridCoverage(bbox, gridMap);
if (coveredCells.Count == 0)
{
return; // 没有覆盖任何网格单元
continue; // 没有覆盖任何网格单元改为continue
}
// 6. 更新网格状态和高度信息(线程安全)
// 6. 更新网格状态和高度信息(移除线程安全
var updatedCells = 0;
foreach (var (x, y) in coveredCells)
{
lock (lockObject) // 确保网格更新的线程安全
// 移除lock单线程不需要锁
var cell = gridMap.Cells[x, y];
// 只更新通道网格,跳过已是障碍物的网格
if (cell.CellType == CategoryAttributeManager.LogisticsElementType. && cell.IsInChannel)
{
var cell = gridMap.Cells[x, y];
// 标记为障碍物
cell.IsWalkable = false;
cell.CellType = CategoryAttributeManager.LogisticsElementType.;
cell.Cost = double.MaxValue;
cell.IsInChannel = false;
cell.RelatedModelItem = item;
// 只更新通道网格,跳过已是障碍物的网格
if (cell.CellType == CategoryAttributeManager.LogisticsElementType. && cell.IsInChannel)
{
// 标记为障碍物
cell.IsWalkable = false;
cell.CellType = CategoryAttributeManager.LogisticsElementType.;
cell.Cost = double.MaxValue;
cell.IsInChannel = false;
cell.RelatedModelItem = item;
// 记录高度信息(支持未来多楼层扩展)
if (cell.PassableHeights == null)
cell.PassableHeights = new List<HeightInterval>();
// 计算相对于通道地面的高度区间
var groundHeight = cell.WorldPosition.Z; // 假设WorldPosition已设置为地面高度
cell.PassableHeights.Add(new HeightInterval(
bbox.Min.Z - groundHeight,
bbox.Max.Z - groundHeight
));
gridMap.Cells[x, y] = cell;
updatedCells++;
}
// 记录高度信息(支持未来多楼层扩展)
if (cell.PassableHeights == null)
cell.PassableHeights = new List<HeightInterval>();
// 计算相对于通道地面的高度区间
var groundHeight = cell.WorldPosition.Z; // 假设WorldPosition已设置为地面高度
cell.PassableHeights.Add(new HeightInterval(
bbox.Min.Z - groundHeight,
bbox.Max.Z - groundHeight
));
gridMap.Cells[x, y] = cell;
updatedCells++;
}
}
if (updatedCells > 0)
{
Interlocked.Increment(ref obstacleItems);
obstacleItems++; // 改为普通递增
}
}
catch (Exception ex)
{
LogManager.Debug($"[包围盒障碍物处理] 处理模型项失败: {item?.DisplayName ?? "NULL"}, {ex.Message}");
}
});
}
// 输出详细统计信息
var elapsed = (DateTime.Now - startTime).TotalMilliseconds;

View File

@ -0,0 +1,17 @@
using System.ComponentModel;
namespace NavisworksTransport.UI.WPF.Interfaces
{
/// <summary>
/// 提供属性变更通知接口,允许外部组件安全地触发属性变更事件
/// 用于替代反射调用,提供更安全的属性变更通知机制
/// </summary>
public interface IPropertyChangeNotifier : INotifyPropertyChanged
{
/// <summary>
/// 触发指定属性的变更通知
/// </summary>
/// <param name="propertyName">属性名称</param>
void NotifyPropertyChanged(string propertyName);
}
}

View File

@ -6,6 +6,8 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
using NavisworksTransport.UI.WPF.ViewModels;
using NavisworksTransport.UI.WPF.Interfaces;
using NavisworksTransport.Utils;
namespace NavisworksTransport.UI.WPF.Services
{
@ -468,13 +470,20 @@ namespace NavisworksTransport.UI.WPF.Services
{
try
{
// 使用反射获取PropertyChanged事件并触发
var propertyChangedField = target.GetType().GetField("PropertyChanged",
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
if (propertyChangedField?.GetValue(target) is PropertyChangedEventHandler handler)
// 优先使用IPropertyChangeNotifier接口安全方式避免反射
if (target is IPropertyChangeNotifier notifier)
{
handler.Invoke(target, new PropertyChangedEventArgs(propertyName));
notifier.NotifyPropertyChanged(propertyName);
}
else
{
// 如果目标对象不实现IPropertyChangeNotifier接口记录警告
// 我们无法安全地触发PropertyChanged事件因为事件只能从内部触发
LogManager.Warning($"目标对象 ({target.GetType().Name}) 未实现IPropertyChangeNotifier接口无法触发属性变更通知: {propertyName}");
LogManager.Info("建议更新ViewModel以实现IPropertyChangeNotifier接口以支持智能数据绑定优化功能");
// 不使用反射,而是直接跳过这个属性的变更通知
// 这是更安全的做法,避免潜在的线程安全问题
}
}
catch (Exception ex)

View File

@ -832,8 +832,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
ClearTemporaryAutoPathMarkers();
if (PathPointRenderPlugin.Instance != null)
{
PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle"); // 清理历史路径但保留网格可视化
LogManager.WriteLog("[自动路径规划] 已清理历史路径对象(保留网格可视化)");
// 检查是否有网格可视化启用,只在启用时保留网格可视化路径
if (_pathPlanningManager?.IsAnyGridVisualizationEnabled == true)
{
PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle");
LogManager.WriteLog("[自动路径规划] 已清理历史路径对象(保留网格可视化)");
}
else
{
PathPointRenderPlugin.Instance.ClearPathsExcept(); // 清理所有路径,不保留任何内容
LogManager.WriteLog("[自动路径规划] 已清理历史路径对象(无网格可视化需保留)");
}
}
// 调用PathPlanningManager的自动路径规划功能
@ -910,8 +919,17 @@ namespace NavisworksTransport.UI.WPF.ViewModels
// 确保失败后也清理所有可能的残留路径对象
if (PathPointRenderPlugin.Instance != null)
{
PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle");
LogManager.WriteLog("[自动路径规划] 规划失败后已清理渲染对象(保留网格可视化)");
// 检查是否有网格可视化启用,只在启用时保留网格可视化路径
if (_pathPlanningManager?.IsAnyGridVisualizationEnabled == true)
{
PathPointRenderPlugin.Instance.ClearPathsExcept("grid_visualization_all", "grid_visualization_channel", "grid_visualization_unknown", "grid_visualization_obstacle");
LogManager.WriteLog("[自动路径规划] 规划失败后已清理渲染对象(保留网格可视化)");
}
else
{
PathPointRenderPlugin.Instance.ClearPathsExcept(); // 清理所有路径,不保留任何内容
LogManager.WriteLog("[自动路径规划] 规划失败后已清理渲染对象(无网格可视化需保留)");
}
}
}
}, "自动路径规划");
@ -1902,40 +1920,18 @@ namespace NavisworksTransport.UI.WPF.ViewModels
{
LogManager.WriteLog("[事件清理] 开始清理自动路径事件订阅");
// 移除所有可能的OnAutoPathMouseClicked订阅
// 简单安全地移除事件订阅
// 由于C#事件处理机制,多次取消订阅同一个处理程序是安全的
// 即使处理程序未订阅,取消操作也不会抛出异常
PathClickToolPlugin.MouseClicked -= OnAutoPathMouseClicked;
// 使用反射检查并清理重复订阅
var mouseClickedEvent = typeof(PathClickToolPlugin).GetEvent("MouseClicked");
if (mouseClickedEvent != null)
{
var eventField = typeof(PathClickToolPlugin).GetField("MouseClicked",
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
if (eventField != null)
{
var currentDelegate = (MulticastDelegate)eventField.GetValue(null);
if (currentDelegate != null)
{
var invocationList = currentDelegate.GetInvocationList();
LogManager.WriteLog($"[事件清理] 发现 {invocationList.Length} 个事件订阅者");
foreach (var handler in invocationList)
{
if (handler.Target == this && handler.Method.Name == "OnAutoPathMouseClicked")
{
PathClickToolPlugin.MouseClicked -= (EventHandler<PickItemResult>)handler;
LogManager.WriteLog($"[事件清理] 移除了PathEditingViewModel.OnAutoPathMouseClicked订阅");
}
}
}
}
}
LogManager.WriteLog("[事件清理] 已安全移除PathClickToolPlugin.MouseClicked事件订阅");
LogManager.WriteLog("[事件清理] 自动路径事件订阅清理完成");
}
catch (Exception ex)
{
LogManager.Error($"[事件清理] 清理自动路径事件订阅失败: {ex.Message}");
LogManager.Error($"[事件清理] 清理自动路径事件订阅失败: {ex.Message}", ex);
// 记录异常但不抛出,避免影响主流程
}
}
@ -2466,48 +2462,36 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// <returns>初始化是否成功</returns>
private bool ForceReinitializeToolPlugin(bool subscribeToEvents = false)
{
LogManager.Info($"开始强制重新初始化ToolPlugin以确保获得鼠标焦点订阅事件: {subscribeToEvents}");
try
try
{
// 使用反射调用私有方法强制重新激活ToolPlugin
var deactivateMethod = _pathPlanningManager.GetType().GetMethod("DeactivateToolPlugin",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (deactivateMethod != null)
{
deactivateMethod.Invoke(_pathPlanningManager, null);
LogManager.Info("已停用ToolPlugin");
}
LogManager.Info($"开始强制重新初始化ToolPlugin以确保获得鼠标焦点订阅事件: {subscribeToEvents}");
// 重置激活状态标志
var isActiveField = _pathPlanningManager.GetType().GetField("_isToolPluginActive",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (isActiveField != null)
// 使用PathPlanningManager的公共方法避免反射调用
if (_pathPlanningManager != null)
{
isActiveField.SetValue(_pathPlanningManager, false);
LogManager.Info("已重置ToolPlugin激活状态标志");
}
// 重新激活ToolPlugin根据参数决定是否订阅事件
var activateMethod = _pathPlanningManager.GetType().GetMethod("ActivateToolPlugin",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
if (activateMethod != null)
{
// 调用带参数的方法传递subscribeToEvents参数
var result = activateMethod.Invoke(_pathPlanningManager, new object[] { subscribeToEvents });
LogManager.Info($"ToolPlugin重新激活结果事件订阅: {subscribeToEvents}: {result}");
if (!(bool)result)
bool result = _pathPlanningManager.ForceReinitializeToolPlugin(subscribeToEvents);
LogManager.Info($"ToolPlugin重新初始化结果事件订阅: {subscribeToEvents}: {result}");
if (result)
{
LogManager.Error("ToolPlugin激活失败");
return false;
LogManager.Info("ToolPlugin重新初始化成功已获得鼠标焦点");
}
else
{
LogManager.Error("ToolPlugin初始化失败");
}
return result;
}
else
{
LogManager.Error("PathPlanningManager为null无法重新初始化ToolPlugin");
return false;
}
LogManager.Info("ToolPlugin重新初始化成功已获得鼠标焦点");
return true;
}
catch (Exception ex)
{
LogManager.Error($"重新初始化ToolPlugin失败: {ex.Message}");
LogManager.Error($"重新初始化ToolPlugin失败: {ex.Message}", ex);
return false;
}
}

View File

@ -7,6 +7,7 @@ using System.Threading.Tasks;
using NavisworksTransport.Core;
using NavisworksTransport.Utils;
using NavisworksTransport.UI.WPF.Services;
using NavisworksTransport.UI.WPF.Interfaces;
namespace NavisworksTransport.UI.WPF.ViewModels
{
@ -15,7 +16,7 @@ namespace NavisworksTransport.UI.WPF.ViewModels
/// 集成UIStateManager提供线程安全的属性更新和防重入机制
/// 集成智能数据绑定优化功能,支持延迟更新、批量更新和条件更新
/// </summary>
public abstract class ViewModelBase : INotifyPropertyChanged
public abstract class ViewModelBase : IPropertyChangeNotifier
{
#region
@ -145,6 +146,15 @@ namespace NavisworksTransport.UI.WPF.ViewModels
}
}
/// <summary>
/// 公共接口:触发指定属性的变更通知(供外部组件使用,替代反射调用)
/// </summary>
/// <param name="propertyName">属性名称</param>
public virtual void NotifyPropertyChanged(string propertyName)
{
OnPropertyChanged(propertyName);
}
/// <summary>
/// 同步触发属性变更通知确保在UI线程上执行完成
/// </summary>