如何在属性变更时更新Spectre.Console的LiveDisplay?
问题描述
我正在开发基于COM连接的设备控制器,包含监听、写入、状态查询三个线程,计划用Spectre.Console的LiveDisplay实时输出各线程状态(运行/暂停)及测试计时器。
目前遇到的核心问题:线程状态属性变更时无法更新控制台——更新需要每次状态变化调用ctx.Refresh(),但找不到在AnsiConsole.Start()方法外部调用该方法的途径。
我采用了一个实现INotifyPropertyChanged的独立类存储状态,线程向其传递值,但不确定这个设计是否合理,后续考虑将其与控制器类合并。
现有代码
ControllerData(状态存储类)
internal class ControllerData : INotifyPropertyChanged { private string _writeThreadState = ""; private string _readThreadState = ""; private string _stateThreadState = ""; private string _actionMessage = ""; public String WriteThreadState { get => _writeThreadState; set { _writeThreadState = value; OnPropertyChanged(nameof(WriteThreadState)); } } public String ReadThreadState { get => _readThreadState; set { _readThreadState = value; OnPropertyChanged(nameof(ReadThreadState)); } } public String StateThreadState { get => _stateThreadState; set { _stateThreadState = value; OnPropertyChanged(nameof(StateThreadState)); } } public String ActionMessage { get => _actionMessage; set { _actionMessage = value; OnPropertyChanged(nameof(ActionMessage)); } } public StringBuilder Output { get; set; } = new StringBuilder(); public event PropertyChangedEventHandler? PropertyChanged; [NotifyPropertyChangedInvocator] protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
Logger类(控制台输出类)
internal class Logger { private ControllerData data; public Logger(Controller cont) { data = cont.loggingData; data.PropertyChanged += ControllerDataPropertyChangedHandler; } private void ControllerDataPropertyChangedHandler(object sender, PropertyChangedEventArgs e) { //refresh console } public void Start() { Table table = new Table() .Border(TableBorder.Rounded) .AddColumn("Thread") .AddColumn("State"); table.AddRow("Listen Thread", data.ReadThreadState); table.AddRow("Write Thread", data.WriteThreadState); table.AddRow("Check state Thread", data.StateThreadState); AnsiConsole.Live(table) .Start(ctx => { ctx.Refresh(); }); } }
主函数
static void Main(string[] args) { ComConnector connector = new ComConnector(); try { Controller cont = new Controller(connector); Logger log = new Logger(cont); log.Start(); cont.StartMessaging(20000); } catch (Exception e) { Console.WriteLine(e); throw; } }
解决方案
设计合理性说明
你当前用独立类存储状态的设计是合理的,符合单一职责原则——状态管理和业务逻辑分离,后续合并到控制器类也完全可行,不影响当前问题的修复。
核心修改:保存LiveContext并绑定状态变更
要解决ctx.Refresh()无法外部调用的问题,只需在AnsiConsole.Live().Start()内部把LiveDisplayContext实例保存下来,让PropertyChanged事件可以直接调用它。同时要注意线程安全,因为状态变更来自不同的工作线程。
修改后的Logger类
internal class Logger { private ControllerData _data; private LiveDisplayContext? _liveContext; private readonly Table _table; private readonly Controller _controller; public Logger(Controller cont) { _controller = cont; _data = cont.loggingData; _data.PropertyChanged += ControllerDataPropertyChangedHandler; // 提前初始化表格,避免重复创建 _table = new Table() .Border(TableBorder.Rounded) .AddColumn("Thread") .AddColumn("State"); } private void ControllerDataPropertyChangedHandler(object sender, PropertyChangedEventArgs e) { // 根据变更的属性更新对应表格单元格 switch (e.PropertyName) { case nameof(ControllerData.ReadThreadState): _table.UpdateCell(0, 1, _data.ReadThreadState); break; case nameof(ControllerData.WriteThreadState): _table.UpdateCell(1, 1, _data.WriteThreadState); break; case nameof(ControllerData.StateThreadState): _table.UpdateCell(2, 1, _data.StateThreadState); break; } // 确保在LiveDisplay的线程执行刷新,避免跨线程问题 _liveContext?.Post(ctx => ((LiveDisplayContext)ctx!).Refresh(), _liveContext); } public void Start() { // 初始化表格初始状态 _table.AddRow("Listen Thread", _data.ReadThreadState); _table.AddRow("Write Thread", _data.WriteThreadState); _table.AddRow("Check state Thread", _data.StateThreadState); AnsiConsole.Live(_table) .Start(ctx => { _liveContext = ctx; // 阻塞等待控制器线程完成,否则LiveDisplay会立即退出 _controller.WaitForThreadsCompletion(); }); } }
配套修改(Controller类)
需要在Controller中添加等待线程完成的方法,避免LiveDisplay启动后立即退出:
internal class Controller { public ControllerData loggingData { get; } = new ControllerData(); private readonly List<Thread> _workerThreads = new(); public void StartMessaging(int duration) { // 示例:启动三个线程 _workerThreads.Add(new Thread(ListenThread) { Name = "Listen" }); _workerThreads.Add(new Thread(WriteThread) { Name = "Write" }); _workerThreads.Add(new Thread(StateCheckThread) { Name = "StateCheck" }); _workerThreads.ForEach(t => t.Start()); } // 添加此方法,等待所有工作线程结束 public void WaitForThreadsCompletion() { foreach (var thread in _workerThreads) { thread.Join(); } } // 省略原有线程实现... private void ListenThread() { /* ... */ } private void WriteThread() { /* ... */ } private void StateCheckThread() { /* ... */ } }
关键注意点
- 线程安全:使用
LiveDisplayContext.Post()方法,确保刷新操作在LiveDisplay的线程执行,避免跨线程操作异常。 - LiveDisplay生命周期:
AnsiConsole.Live().Start()的委托会阻塞到内部逻辑完成,所以必须等待控制器的工作线程结束,否则LiveDisplay会直接关闭。 - 表格更新效率:用
UpdateCell()直接修改对应单元格,比重新创建表格更高效。
内容的提问来源于stack exchange,提问作者N A
相关产品推荐
相关产品推荐

