using OxyPlot; using OxyPlot.Axes; using OxyPlot.Legends; using OxyPlot.Series; using System.Collections.ObjectModel; using System.Reflection; using System.Windows.Input; using System.Windows.Threading; using UIShare.GlobalVariable; using UIShare.PubEvent; using UIShare.ViewModelBase; using Prism.Mvvm; using Common.Attributes; namespace MonitorModule.ViewModels { public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable { #region 属性 public bool KeepAlive => true; public ScopedContext _scopedContext { get; set; } public GlobalInfo _globalInfo { get; set; } public string TestStatus { get => _testStatus; set => SetProperty(ref _testStatus, value); } public PlotModel Plot { get; } /// 已添加的监测通道列表 public ObservableCollection Channels { get; } = new(); /// 可添加的设备方法列表(供用户选择) public ObservableCollection AvailableMethods { get; } = new(); private MonitorChannel? _selectedChannel; public MonitorChannel? SelectedChannel { get => _selectedChannel; set => SetProperty(ref _selectedChannel, value); } private AvailableMethodItem? _selectedAvailableMethod; public AvailableMethodItem? SelectedAvailableMethod { get => _selectedAvailableMethod; set => SetProperty(ref _selectedAvailableMethod, value); } public string StatusMessage { get => _statusMessage; set => SetProperty(ref _statusMessage, value); } #endregion #region 命令 public ICommand AddChannelCommand { get; } public ICommand DeleteChannelCommand { get; } public ICommand ResetViewCommand { get; } public ICommand RefreshDataCommand { get; } public ICommand RefreshCommand { get; } #endregion #region 私有字段 private bool IsInitiated = false; private IScopedProvider _scope; private DeviceManager? _deviceManager; private static readonly OxyColor[] _palette = { OxyColors.SteelBlue, OxyColors.IndianRed, OxyColors.SeaGreen, OxyColors.DarkOrange, OxyColors.MediumPurple, OxyColors.Goldenrod, OxyColors.Teal, OxyColors.Crimson, OxyColors.OliveDrab }; private int _colorIndex; private readonly DispatcherTimer _sampleTimer; private double _sampleTime; private const double _sampleInterval = 0.1; // 100ms private string _testStatus = string.Empty; private string _statusMessage = "图表已就绪,暂无监测项"; /// 反射方法缓存:Channel → (MethodInfo, DeviceInstance) private readonly Dictionary _methodCache = new(); /// 方法名前缀:匹配这些开头的公共方法被视为可监测项 private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" }; #endregion public MonitorViewModel(IContainerExtension container) : base(container) { _globalInfo = container.Resolve(); Plot = BuildEmptyPlot(); AddChannelCommand = new DelegateCommand(OnAddChannel); DeleteChannelCommand = new DelegateCommand(OnDeleteChannel); ResetViewCommand = new DelegateCommand(OnResetView); RefreshDataCommand = new DelegateCommand(OnRefreshData); RefreshCommand = new DelegateCommand(OnExpand); _sampleTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(_sampleInterval) }; _sampleTimer.Tick += OnSampleTick; } public void Dispose() { _sampleTimer?.Stop(); _scope?.Dispose(); } #region PlotModel 构建 private static PlotModel BuildEmptyPlot() { var pm = new PlotModel { Title = string.Empty, PlotAreaBorderColor = OxyColors.LightGray, Background = OxyColors.White }; pm.Axes.Add(new LinearAxis { Position = AxisPosition.Bottom, Title = "时间 (s)", MajorGridlineStyle = LineStyle.Dot, MinorGridlineStyle = LineStyle.None }); pm.Axes.Add(new LinearAxis { Position = AxisPosition.Left, Title = "值", MajorGridlineStyle = LineStyle.Dot, MinorGridlineStyle = LineStyle.None }); pm.Legends.Add(new Legend { LegendPosition = LegendPosition.RightTop, LegendBackground = OxyColor.FromAColor(200, OxyColors.White), LegendBorder = OxyColors.LightGray }); return pm; } #endregion #region 设备方法发现 /// /// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。 /// private void DiscoverAvailableMethods() { AvailableMethods.Clear(); if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0) { StatusMessage = "当前工位无可用设备"; return; } foreach (var kvp in _deviceManager.DeviceMap) { string deviceName = kvp.Key; var device = kvp.Value; var deviceType = device.GetType(); // 核心优化:直接通过特性和参数过滤方法 var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance) .Where(m => { // 1. 核心过滤:必须包含 [Monitorable] 特性 var attribute = m.GetCustomAttribute(); if (attribute == null) return false; // 2. 返回类型校验(如果强制要求是 Task) if (m.ReturnType != typeof(Task)) return false; // 3. 参数校验:只取无参或仅有 CancellationToken 参数的方法 var parms = m.GetParameters(); bool validParams = parms.Length == 0 || (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); return validParams; }); foreach (var method in methods) { // 如果你在特性里写了 Description,这里可以拿出来赋给 DisplayName var attr = method.GetCustomAttribute(); string displayName = !string.IsNullOrEmpty(attr?.Description) ? $"{deviceName}.{attr.Description}" : $"{deviceName}.{method.Name}"; var item = new AvailableMethodItem { DeviceName = deviceName, MethodName = method.Name, DisplayName = displayName, // 优先使用特性的中文描述 MethodInfo = method, Device = device }; AvailableMethods.Add(item); } } StatusMessage = AvailableMethods.Count > 0 ? $"发现 {AvailableMethods.Count} 个可监测项" : "未发现可监测的设备方法"; } #endregion #region 命令处理 private void OnAddChannel() { var method = SelectedAvailableMethod; if (method == null) { StatusMessage = "请先在列表中选择一个监测项"; return; } // 防止重复添加同一设备方法 if (Channels.Any(c => c.DeviceName == method.DeviceName && c.MethodName == method.MethodName)) { StatusMessage = $"监测项 [{method.DisplayName}] 已存在"; return; } var color = _palette[_colorIndex % _palette.Length]; _colorIndex++; var channel = new MonitorChannel { DeviceName = method.DeviceName, MethodName = method.MethodName, DisplayName = method.DisplayName, Color = color, IsMonitored = true, IsDisplayed = true }; // 创建 Series 并加入 Plot channel.Series = new LineSeries { Title = channel.DisplayName, Color = color, StrokeThickness = 1.5 }; Plot.Series.Add(channel.Series); // 缓存反射信息 _methodCache[channel] = (method.MethodInfo, method.Device); // 订阅属性变化:IsDisplayed 控制显示/隐藏,MathExpression 控制数学变换 channel.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(MonitorChannel.IsDisplayed)) OnChannelDisplayChanged(channel); else if (e.PropertyName == nameof(MonitorChannel.MathExpression)) OnChannelMathChanged(channel); }; Channels.Add(channel); SelectedChannel = channel; Plot.InvalidatePlot(true); StatusMessage = $"已添加监测项 [{channel.DisplayName}],当前共 {Channels.Count} 项"; } private void OnDeleteChannel() { var target = SelectedChannel ?? Channels.LastOrDefault(); if (target == null) { StatusMessage = "无可删除的监测项"; return; } if (target.Series != null) { Plot.Series.Remove(target.Series); } _methodCache.Remove(target); Channels.Remove(target); SelectedChannel = Channels.LastOrDefault(); Plot.InvalidatePlot(true); StatusMessage = $"已删除监测项 [{target.DisplayName}],剩余 {Channels.Count} 项"; } private void OnResetView() { Plot.ResetAllAxes(); Plot.InvalidatePlot(false); StatusMessage = "视图已按数据范围复原"; } private void OnRefreshData() { Plot.InvalidatePlot(true); StatusMessage = $"已刷新({DateTime.Now:HH:mm:ss})"; } private void OnExpand() { if (string.IsNullOrEmpty(TestStatus)) return; _globalInfo.CurrentScope = TestStatus; _eventAggregator.GetEvent().Publish(TestStatus); } #endregion #region 显示/隐藏切换 /// /// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除 /// public void OnChannelDisplayChanged(MonitorChannel channel) { if (channel.IsDisplayed) { // 从 false → true:创建 Series,回填最近数据 if (channel.Series == null) { channel.Series = new LineSeries { Title = channel.DisplayName, Color = channel.Color, StrokeThickness = 1.5 }; // 回填最近 200 个数据点 var recent = channel.DataPoints.ToArray(); var startIdx = Math.Max(0, recent.Length - 200); for (int i = startIdx; i < recent.Length; i++) { channel.Series.Points.Add(new DataPoint(recent[i].Time, recent[i].DisplayValue)); } Plot.Series.Add(channel.Series); Plot.InvalidatePlot(true); } } else { // 从 true → false:移除 Series,数据继续记录 if (channel.Series != null) { Plot.Series.Remove(channel.Series); channel.Series = null; Plot.InvalidatePlot(true); } } } /// /// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue /// public void OnChannelMathChanged(MonitorChannel channel) { if (channel.Series == null) return; // 用新表达式重算 Series 中的点 var points = channel.DataPoints.ToArray(); channel.Series.Points.Clear(); var startIdx = Math.Max(0, points.Length - 200); for (int i = startIdx; i < points.Length; i++) { channel.Series.Points.Add(new DataPoint(points[i].Time, points[i].DisplayValue)); } Plot.InvalidatePlot(true); } #endregion #region 采样定时器 private async void OnSampleTick(object? sender, EventArgs e) { _sampleTime += _sampleInterval; if (Channels.Count == 0 || _methodCache.Count == 0) return; foreach (var channel in Channels) { if (!channel.IsMonitored) continue; if (!_methodCache.TryGetValue(channel, out var entry)) continue; try { var (method, device) = entry; // 调用设备方法获取返回值 var task = (Task)method.Invoke(device, null)!; var raw = await task.ConfigureAwait(true); // 尝试解析为 double if (double.TryParse(raw, out double value)) { channel.Record(_sampleTime, value); } } catch { // 设备读取失败时跳过该通道本次采样 } } // 让 X 轴跟随最新数据滚动 var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom); if (xAxis != null) { double window = 200 * _sampleInterval; xAxis.Minimum = Math.Max(0, _sampleTime - window); xAxis.Maximum = _sampleTime + 0.5; } Plot.InvalidatePlot(true); } #endregion #region 重写 public override void OnNavigatedTo(NavigationContext navigationContext) { base.OnNavigatedTo(navigationContext); if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name")) { TestStatus = navigationContext.Parameters.GetValue("Name"); Plot.Title = $"监控 - {TestStatus}"; Plot.InvalidatePlot(false); _scope = _globalInfo.ScopeDic[TestStatus]; _scopedContext = _scope.Resolve(); _deviceManager = _scope.Resolve(); IsInitiated = true; // 扫描可用监测项 DiscoverAvailableMethods(); // 启动采样定时器 _sampleTimer.Start(); } } #endregion } /// /// 可用设备方法项(供用户选择添加为监测通道) /// public class AvailableMethodItem { public string DeviceName { get; set; } = string.Empty; public string MethodName { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; public MethodInfo MethodInfo { get; set; } = null!; public object Device { get; set; } = null!; } }