Files
ADP/MonitorModule/ViewModels/MonitorViewModel.cs
2026-06-26 14:01:48 +08:00

451 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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; }
/// <summary>已添加的监测通道列表</summary>
public ObservableCollection<MonitorChannel> Channels { get; } = new();
/// <summary>可添加的设备方法列表(供用户选择)</summary>
public ObservableCollection<AvailableMethodItem> 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 = "图表已就绪,暂无监测项";
/// <summary>反射方法缓存Channel → (MethodInfo, DeviceInstance)</summary>
private readonly Dictionary<MonitorChannel, (MethodInfo Method, object Device)> _methodCache = new();
/// <summary>方法名前缀:匹配这些开头的公共方法被视为可监测项</summary>
private static readonly string[] _methodPrefixes = { "查询", "读取", "获取", "测量" };
#endregion
public MonitorViewModel(IContainerExtension container) : base(container)
{
_globalInfo = container.Resolve<GlobalInfo>();
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
/// <summary>
/// 扫描 DeviceManager.DeviceMap 中所有设备,反射找出可监测的方法。
/// </summary>
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<MonitorableAttribute>();
if (attribute == null) return false;
// 2. 返回类型校验(如果强制要求是 Task<string>
if (m.ReturnType != typeof(Task<string>)) 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<MonitorableAttribute>();
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<ExpandViewEvent>().Publish(TestStatus);
}
#endregion
#region /
/// <summary>
/// 当 Channel.IsDisplayed 变化时调用:控制 Series 的创建/移除
/// </summary>
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);
}
}
}
/// <summary>
/// 当 Channel.MathExpression 变化时调用:重新计算所有已记录数据点的 DisplayValue
/// </summary>
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<string>)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<string>("Name");
Plot.Title = $"监控 - {TestStatus}";
Plot.InvalidatePlot(false);
_scope = _globalInfo.ScopeDic[TestStatus];
_scopedContext = _scope.Resolve<ScopedContext>();
_deviceManager = _scope.Resolve<DeviceManager>();
IsInitiated = true;
// 扫描可用监测项
DiscoverAvailableMethods();
// 启动采样定时器
_sampleTimer.Start();
}
}
#endregion
}
/// <summary>
/// 可用设备方法项(供用户选择添加为监测通道)
/// </summary>
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!;
}
}