添加项目文件。
This commit is contained in:
398
MonitorModule/ViewModels/Dialogs/ValueLimitViewModel.cs
Normal file
398
MonitorModule/ViewModels/Dialogs/ValueLimitViewModel.cs
Normal file
@@ -0,0 +1,398 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Prism.Events;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using UIShare.ViewModelBase;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
namespace MonitorModule.ViewModels.Dialogs
|
||||
{
|
||||
public class ValueLimitViewModel : DialogViewModelBase
|
||||
{
|
||||
#region 属性
|
||||
private string _Title = "报警设置";
|
||||
|
||||
public string Title
|
||||
{
|
||||
get { return _Title; }
|
||||
set { SetProperty(ref _Title, value); }
|
||||
}
|
||||
|
||||
private ValueLimitVM _SelectValueLimitVM;
|
||||
|
||||
public ValueLimitVM SelectValueLimitVM
|
||||
{
|
||||
get => _SelectValueLimitVM;
|
||||
set => SetProperty(ref _SelectValueLimitVM, value);
|
||||
}
|
||||
private ObservableCollection<ValueLimitVM> _ValueLimitList;
|
||||
public ObservableCollection<ValueLimitVM> ValueLimitList
|
||||
{
|
||||
get => _ValueLimitList;
|
||||
set => SetProperty(ref _ValueLimitList, value);
|
||||
}
|
||||
//private ObservableCollection<string> _DeviceSingleList = new();
|
||||
|
||||
//public ObservableCollection<string> DeviceSingleList
|
||||
//{
|
||||
// get => _DeviceSingleList;
|
||||
// set => SetProperty(ref _DeviceSingleList, value);
|
||||
//}
|
||||
//private string _SelectCurve;
|
||||
|
||||
//public string SelectCurve
|
||||
//{
|
||||
// get => _SelectCurve;
|
||||
// set => SetProperty(ref _SelectCurve, value);
|
||||
//}
|
||||
|
||||
#endregion
|
||||
#region 命令
|
||||
public ICommand SaveCommand { get; set; }
|
||||
public ICommand CloseCommand { get; set; }
|
||||
//public ICommand SelectSignalCommand { get; set; }
|
||||
public ICommand DeleteCommand { get; set; }
|
||||
|
||||
#endregion
|
||||
private GlobalInfo _globalInfo { get; set; }
|
||||
private SystemConfig _systemConfig { get; set; }
|
||||
private IScopedProvider _scope { get; set; }
|
||||
private DeviceManager? _deviceManager;
|
||||
|
||||
/// <summary>SignalName → CAN信号标识,用于DBC加载/卸载时清理</summary>
|
||||
private readonly Dictionary<string, (string Fingerprint, string MethodName)> _canSignalMap = new();
|
||||
|
||||
private SubscriptionToken? _dbcLoadedToken;
|
||||
private SubscriptionToken? _dbcUnloadedToken;
|
||||
|
||||
public ValueLimitViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||
{
|
||||
_globalInfo = containerProvider.Resolve<GlobalInfo>();
|
||||
_eventAggregator = containerProvider.Resolve<IEventAggregator>();
|
||||
SaveCommand = new DelegateCommand(OnSave);
|
||||
CloseCommand = new DelegateCommand(OnClose);
|
||||
//SelectSignalCommand = new DelegateCommand(SelectSignal);
|
||||
DeleteCommand = new DelegateCommand(Deletel);
|
||||
}
|
||||
#region 命令处理
|
||||
private void Deletel()
|
||||
{
|
||||
if (SelectValueLimitVM == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
ValueLimitList.Remove(SelectValueLimitVM);
|
||||
SelectValueLimitVM = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void InitData()
|
||||
{
|
||||
ValueLimitList = new ObservableCollection<ValueLimitVM>();
|
||||
//DeviceSingleList = new ObservableCollection<string>();
|
||||
_canSignalMap.Clear();
|
||||
|
||||
// 1. 加载已保存的 ValueLimitList(保留用户设置)
|
||||
if (_systemConfig?.ValueLimitList != null)
|
||||
{
|
||||
foreach (var item in _systemConfig.ValueLimitList)
|
||||
ValueLimitList.Add(item);
|
||||
}
|
||||
|
||||
// 2. 发现设备方法信号并加入 ValueLimitList / DeviceSingleList
|
||||
foreach (var (displayName, fingerprint, methodName) in DiscoverDeviceSignals())
|
||||
{
|
||||
//DeviceSingleList.Add(displayName);
|
||||
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||
}
|
||||
|
||||
// 3. 发现 CAN 信号并加入 ValueLimitList,同时收集有效信号名
|
||||
var validCanSignalNames = new HashSet<string>();
|
||||
foreach (var (displayName, fingerprint, methodName) in DiscoverCanSignals())
|
||||
{
|
||||
_canSignalMap[displayName] = (fingerprint, methodName);
|
||||
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||
validCanSignalNames.Add(displayName);
|
||||
}
|
||||
|
||||
// 4. 清理已不存在于 DBC 中的 CAN 信号(Fingerprint 以 "CAN" 开头且不在有效集合中)
|
||||
var staleCanSignals = ValueLimitList
|
||||
.Where(x => !string.IsNullOrEmpty(x.Fingerprint) &&
|
||||
x.Fingerprint.StartsWith("CAN") &&
|
||||
!validCanSignalNames.Contains(x.SignalName))
|
||||
.ToList();
|
||||
foreach (var item in staleCanSignals)
|
||||
ValueLimitList.Remove(item);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发现当前作用域所有可监测的设备方法信号(与 MonitorViewModel 逻辑一致)。
|
||||
/// 返回 (DisplayName, Fingerprint, MethodName) 列表。
|
||||
/// </summary>
|
||||
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverDeviceSignals()
|
||||
{
|
||||
if (_deviceManager?.DeviceMap == null) yield break;
|
||||
|
||||
var instanceToFp = BuildInstanceToFingerprint();
|
||||
foreach (var kvp in _deviceManager.DeviceMap)
|
||||
{
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
|
||||
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
|
||||
continue;
|
||||
|
||||
var methods = device.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
var parms = m.GetParameters();
|
||||
return parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
});
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||
? $"{deviceName}.{attr.Description}"
|
||||
: $"{deviceName}.{method.Name}";
|
||||
yield return (displayName, fingerprint, method.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建设备实例 → 硬件指纹的反向查找表。
|
||||
/// </summary>
|
||||
private Dictionary<object, string> BuildInstanceToFingerprint()
|
||||
{
|
||||
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||
if (_globalInfo?.HardwarePool == null) return map;
|
||||
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
{
|
||||
var lazy = poolEntry.Value;
|
||||
if (lazy.IsValueCreated && lazy.Value != null)
|
||||
map[lazy.Value] = poolEntry.Key;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发现当前已加载 DBC 中的 CAN 信号(以 ConfigurationList 为白名单)。
|
||||
/// 返回 (DisplayName, Fingerprint, MethodName) 列表。
|
||||
/// </summary>
|
||||
private IEnumerable<(string DisplayName, string Fingerprint, string MethodName)> DiscoverCanSignals()
|
||||
{
|
||||
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) yield break;
|
||||
if (_systemConfig?.ConfigurationList == null) yield break;
|
||||
|
||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||
foreach (var cfg in _systemConfig.ConfigurationList)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
|
||||
if (cfg.Channel < 0 || cfg.Channel >= msgDb.Count) continue;
|
||||
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, (uint)cfg.Channel);
|
||||
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
|
||||
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
||||
|
||||
if (ExistsInDbc(methodName, msgDb[cfg.Channel]))
|
||||
yield return (displayName, fingerprint, methodName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断指定的 MethodName 是否存在于该通道的 DBC 消息数据库中。
|
||||
/// </summary>
|
||||
private static bool ExistsInDbc(string methodName, List<_Msg_> messages)
|
||||
{
|
||||
if (string.IsNullOrEmpty(methodName)) return false;
|
||||
int dotIndex = methodName.IndexOf('.');
|
||||
if (dotIndex <= 0 || dotIndex >= methodName.Length - 1) return false;
|
||||
|
||||
if (!uint.TryParse(methodName.Substring(0, dotIndex), System.Globalization.NumberStyles.HexNumber, null, out uint msgId))
|
||||
return false;
|
||||
string signalName = methodName.Substring(dotIndex + 1);
|
||||
|
||||
return messages.Any(m => m.msg_id == msgId && m.signal_Name != null && m.signal_Name.Contains(signalName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 确保 ValueLimitList 中存在指定信号;不存在则添加默认值。
|
||||
/// </summary>
|
||||
private void EnsureValueLimit(string fingerprint, string displayName, string signalName, string methodName = "")
|
||||
{
|
||||
if (ValueLimitList.Any(x => x.SignalName == signalName)) return;
|
||||
|
||||
ValueLimitList.Add(new ValueLimitVM
|
||||
{
|
||||
DisplayName = displayName,
|
||||
Fingerprint = fingerprint,
|
||||
SignalName = signalName,
|
||||
MethodName = methodName,
|
||||
Upper = 9999,
|
||||
Lower = -9999,
|
||||
UpperExtreme = 9999,
|
||||
LowerExtreme = -9999,
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 ValueLimitList 和 DeviceSingleList 中移除指定信号。
|
||||
/// </summary>
|
||||
private void RemoveValueLimit(string signalName)
|
||||
{
|
||||
var item = ValueLimitList.FirstOrDefault(x => x.SignalName == signalName);
|
||||
if (item != null) ValueLimitList.Remove(item);
|
||||
|
||||
//if (DeviceSingleList.Contains(signalName))
|
||||
// DeviceSingleList.Remove(signalName);
|
||||
}
|
||||
|
||||
//private void SelectSignal()
|
||||
//{
|
||||
// if (ValueLimitList.Where(x => x.SignalName == SelectCurve).Count() > 0)
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
// ValueLimitList.Add(new ValueLimitVM
|
||||
// {
|
||||
// SignalName = SelectCurve,
|
||||
// Upper = 9999,
|
||||
// Lower = -9999,
|
||||
// UpperExtreme = 9999,
|
||||
// LowerExtreme = -9999,
|
||||
// });
|
||||
//}
|
||||
|
||||
private void OnClose()
|
||||
{
|
||||
RequestClose.Invoke();
|
||||
}
|
||||
|
||||
private void OnSave()
|
||||
{
|
||||
_systemConfig.ValueLimitList = ValueLimitList;
|
||||
ConfigService.Save(_systemConfig);
|
||||
}
|
||||
#endregion
|
||||
public override void OnDialogOpened(IDialogParameters parameters)
|
||||
{
|
||||
// 1. 优先执行基类的打开逻辑(如果基类有需要初始化的通用事务)
|
||||
base.OnDialogOpened(parameters);
|
||||
|
||||
// 2. 解析参数
|
||||
if (parameters != null && parameters.ContainsKey("Scope")) // 修复:Prism 中应使用 ContainsKey
|
||||
{
|
||||
_scope = parameters.GetValue<IScopedProvider>("Scope");
|
||||
_systemConfig = _scope.Resolve<SystemConfig>();
|
||||
_deviceManager = _scope.Resolve<DeviceManager>();
|
||||
|
||||
// 3. 订阅 DBC 加载/卸载事件
|
||||
_dbcLoadedToken = _eventAggregator.GetEvent<DBCLoadedEvent>()
|
||||
.Subscribe(OnDbcLoaded, ThreadOption.UIThread);
|
||||
_dbcUnloadedToken = _eventAggregator.GetEvent<DBCUnloadedEvent>()
|
||||
.Subscribe(OnDbcUnloaded, ThreadOption.UIThread);
|
||||
|
||||
// 4. 初始化数据
|
||||
InitData();
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnDialogClosed()
|
||||
{
|
||||
base.OnDialogClosed();
|
||||
|
||||
// 取消 DBC 事件订阅,避免内存泄漏
|
||||
if (_dbcLoadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCLoadedEvent>().Unsubscribe(_dbcLoadedToken);
|
||||
if (_dbcUnloadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCUnloadedEvent>().Unsubscribe(_dbcUnloadedToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCLoadedEvent 回调:清理该通道中不存在于 DBC 的 CAN 信号限制项。
|
||||
/// </summary>
|
||||
private void OnDbcLoaded(DBCLoadedArgs args)
|
||||
{
|
||||
if (args.Scope != _systemConfig?.Title) return;
|
||||
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
|
||||
|
||||
int channel = (int)args.Channel;
|
||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||
if (channel < 0 || channel >= msgDb.Count) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||
|
||||
// 1. 移除该通道中已失效的 CAN 信号限制项
|
||||
var staleSignals = _canSignalMap
|
||||
.Where(kvp => kvp.Value.Fingerprint == fingerprint &&
|
||||
!ExistsInDbc(kvp.Value.MethodName, msgDb[channel]))
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var signalName in staleSignals)
|
||||
{
|
||||
RemoveValueLimit(signalName);
|
||||
_canSignalMap.Remove(signalName);
|
||||
}
|
||||
|
||||
// 2. 根据 ConfigurationList 补充新加载的有效信号
|
||||
if (_systemConfig?.ConfigurationList != null)
|
||||
{
|
||||
foreach (var cfg in _systemConfig.ConfigurationList.Where(c => c.Channel == channel))
|
||||
{
|
||||
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
|
||||
|
||||
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
|
||||
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
||||
|
||||
if (ExistsInDbc(methodName, msgDb[channel]))
|
||||
{
|
||||
_canSignalMap[displayName] = (fingerprint, methodName);
|
||||
//if (!DeviceSingleList.Contains(displayName))
|
||||
// DeviceSingleList.Add(displayName);
|
||||
EnsureValueLimit(fingerprint, displayName, displayName, methodName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCUnloadedEvent 回调:删除该通道所有 CAN 信号的限制项。
|
||||
/// </summary>
|
||||
private void OnDbcUnloaded(DBCUnloadedArgs args)
|
||||
{
|
||||
if (args.Scope != _systemConfig?.Title) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager.GetCanDeviceFingerprint();
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||
var toRemove = _canSignalMap
|
||||
.Where(kvp => kvp.Value.Fingerprint == fingerprint)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var signalName in toRemove)
|
||||
{
|
||||
RemoveValueLimit(signalName);
|
||||
_canSignalMap.Remove(signalName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
852
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
852
MonitorModule/ViewModels/MonitorViewModel.cs
Normal file
@@ -0,0 +1,852 @@
|
||||
using Common.Attributes;
|
||||
using Model.Entity;
|
||||
using Model.Models;
|
||||
using NLog;
|
||||
using OxyPlot;
|
||||
using OxyPlot.Axes;
|
||||
using OxyPlot.Legends;
|
||||
using OxyPlot.Series;
|
||||
using Service.Interface;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Threading;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.UIViewModel;
|
||||
using UIShare.ViewModelBase;
|
||||
using ZLGUSBCANFD;
|
||||
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
public class MonitorViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
#region 属性
|
||||
private IMonitorValueService _monitorValueService;
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; set; } = null!;
|
||||
public GlobalInfo _globalInfo { get; set; }
|
||||
|
||||
private string _testStatus = string.Empty;
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
set => SetProperty(ref _testStatus, value);
|
||||
}
|
||||
|
||||
public PlotModel Plot { get; }
|
||||
|
||||
/// <summary>已添加的监测通道列表</summary>
|
||||
public ObservableCollection<MonitorChannelVM> Channels
|
||||
{
|
||||
get => _systemConfig?.Channels ?? new ObservableCollection<MonitorChannelVM>();
|
||||
set
|
||||
{
|
||||
if (_systemConfig != null && _systemConfig.Channels != value)
|
||||
{
|
||||
_systemConfig.Channels = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>可添加的设备方法列表(供用户选择)</summary>
|
||||
public ObservableCollection<AvailableMethodItem> AvailableMethods { get; } = new();
|
||||
|
||||
private MonitorChannelVM? _selectedChannel;
|
||||
public MonitorChannelVM? SelectedChannel
|
||||
{
|
||||
get => _selectedChannel;
|
||||
set => SetProperty(ref _selectedChannel, value);
|
||||
}
|
||||
|
||||
private AvailableMethodItem? _selectedAvailableMethod;
|
||||
public AvailableMethodItem? SelectedAvailableMethod
|
||||
{
|
||||
get => _selectedAvailableMethod;
|
||||
set => SetProperty(ref _selectedAvailableMethod, value);
|
||||
}
|
||||
|
||||
private string _statusMessage = "图表已就绪,暂无监测项";
|
||||
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; }
|
||||
public ICommand SaveCommand { get; }
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
private bool IsInitiated = false;
|
||||
private IScopedProvider? _scope;
|
||||
private DeviceManager? _deviceManager;
|
||||
private SystemConfig? _systemConfig;
|
||||
private CancellationTokenSource? _monitorCTS = new();
|
||||
|
||||
// 颜色调色盘
|
||||
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;
|
||||
|
||||
/// <summary>设备数据广播器(全局单例)</summary>
|
||||
private HardwareDataBroadcaster? _broadcaster;
|
||||
|
||||
/// <summary>CAN 信号广播器(全局单例)</summary>
|
||||
private CANSignalBroadcaster? _canSignalBroadcaster;
|
||||
|
||||
/// <summary>用于 OxyPlot X 轴相对秒数</summary>
|
||||
private readonly Stopwatch _stopwatch = new();
|
||||
|
||||
/// <summary>OxyPlot 刷新定时器(仅负责滚动 X 轴 + InvalidatePlot)</summary>
|
||||
private readonly DispatcherTimer _plotRefreshTimer;
|
||||
|
||||
/// <summary>事件订阅令牌,用于 Dispose 时取消订阅</summary>
|
||||
private SubscriptionToken? _subscriptionToken;
|
||||
private SubscriptionToken? _dbcLoadedToken;
|
||||
private SubscriptionToken? _dbcUnloadedToken;
|
||||
|
||||
// ==========================================
|
||||
// 批量入库核心结构
|
||||
// ==========================================
|
||||
private readonly ConcurrentQueue<MonitorValueEntity> _insertQueue = new();
|
||||
private Task? _dbFlushTask;
|
||||
private const int BulkInsertThreshold = 50;
|
||||
private int _isFlushing = 0;
|
||||
#endregion
|
||||
|
||||
public MonitorViewModel(IContainerExtension container) : base(container)
|
||||
{
|
||||
_globalInfo = container.Resolve<GlobalInfo>();
|
||||
_monitorValueService = container.Resolve<IMonitorValueService>();
|
||||
|
||||
Plot = BuildEmptyPlot();
|
||||
|
||||
AddChannelCommand = new DelegateCommand(OnAddChannel);
|
||||
DeleteChannelCommand = new DelegateCommand(OnDeleteChannel);
|
||||
ResetViewCommand = new DelegateCommand(OnResetView);
|
||||
RefreshDataCommand = new DelegateCommand(OnRefreshData);
|
||||
RefreshCommand = new DelegateCommand(OnExpand);
|
||||
SaveCommand = new DelegateCommand(OnSave);
|
||||
|
||||
// OxyPlot 刷新定时器(1000ms,只负责视觉更新)
|
||||
_plotRefreshTimer = new DispatcherTimer(DispatcherPriority.Background)
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(1000)
|
||||
};
|
||||
_plotRefreshTimer.Tick += OnPlotRefreshTick;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_monitorCTS?.Cancel();
|
||||
_monitorCTS?.Dispose();
|
||||
_monitorCTS = null;
|
||||
|
||||
_plotRefreshTimer.Stop();
|
||||
_plotRefreshTimer.Tick -= OnPlotRefreshTick;
|
||||
_stopwatch.Stop();
|
||||
|
||||
// 取消 EventAggregator 订阅
|
||||
if (_subscriptionToken != null)
|
||||
_eventAggregator.GetEvent<HardwareDataReportedEvent>().Unsubscribe(_subscriptionToken);
|
||||
if (_dbcLoadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCLoadedEvent>().Unsubscribe(_dbcLoadedToken);
|
||||
if (_dbcUnloadedToken != null)
|
||||
_eventAggregator.GetEvent<DBCUnloadedEvent>().Unsubscribe(_dbcUnloadedToken);
|
||||
|
||||
if (_dbFlushTask != null)
|
||||
{
|
||||
try { _dbFlushTask.Wait(TimeSpan.FromSeconds(1.5)); } catch { }
|
||||
}
|
||||
|
||||
|
||||
// 3. 释放容器作用域
|
||||
_scope?.Dispose();
|
||||
Channels.Clear();
|
||||
AvailableMethods.Clear();
|
||||
}
|
||||
private void OnSave()
|
||||
{
|
||||
// 将当前 Channels 序列化为 MonitorChannelConfig 列表保存到 MonitorChannels
|
||||
if (_systemConfig != null)
|
||||
{
|
||||
_systemConfig.MonitorChannels = new ObservableCollection<MonitorChannelConfig>(Channels
|
||||
.Select(c => new MonitorChannelConfig
|
||||
{
|
||||
Fingerprint = c.Fingerprint,
|
||||
MethodName = c.MethodName,
|
||||
IsDisplayed = c.IsDisplayed
|
||||
}));
|
||||
}
|
||||
ConfigService.Save(_systemConfig);
|
||||
}
|
||||
#region 事件驱动:接收广播数据
|
||||
/// <summary>
|
||||
/// 由 HardwareDataBroadcaster 通过 EventAggregator 广播触发。
|
||||
/// 已通过 ThreadOption.UIThread 确保在 UI 线程执行。
|
||||
/// 通过 HardwareFingerprint + MethodName 匹配监测通道。
|
||||
/// </summary>
|
||||
private void OnHardwareDataReceived(HardwareReportArgs args)
|
||||
{
|
||||
// 仅处理属于当前 Scope 的事件
|
||||
if (string.IsNullOrEmpty(TestStatus) || args.Scope != TestStatus) return;
|
||||
|
||||
// 以硬件指纹 + 方法名唯一匹配通道(与逻辑名解耦)
|
||||
var channel = Channels.FirstOrDefault(c =>
|
||||
c.Fingerprint == args.HardwareFingerprint &&
|
||||
c.MethodName == args.MethodName &&
|
||||
c.IsMonitored);
|
||||
|
||||
if (channel == null) return;
|
||||
|
||||
// 记录数据点(线程安全:Record 内部 ConcurrentQueue + Series 操作)
|
||||
channel.Record(args.Time, args.Value);
|
||||
|
||||
// 入队数据库批量写入
|
||||
// CreateTime 使用 args.Time(采样触发时刻),而不是 DateTime.Now(设备响应到达时刻),
|
||||
// 这样即使设备响应有延迟,数据库里的时间戳仍然按设定采样间隔分布。
|
||||
var entity = new MonitorValueEntity
|
||||
{
|
||||
MonitorName = channel.DisplayName,
|
||||
MonitorValue = args.Value,
|
||||
Scope = args.Scope,
|
||||
CreateTime = args.Time,
|
||||
IsDel = 0
|
||||
};
|
||||
_insertQueue.Enqueue(entity);
|
||||
|
||||
if (_insertQueue.Count >= BulkInsertThreshold)
|
||||
{
|
||||
_ = Task.Run(async () => await DoFlushWorkAsync().ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region OxyPlot 视觉刷新定时器
|
||||
private void OnPlotRefreshTick(object? sender, EventArgs e)
|
||||
{
|
||||
if (!_stopwatch.IsRunning) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var xAxis = Plot.Axes.FirstOrDefault(a => a.Position == AxisPosition.Bottom);
|
||||
if (xAxis != null)
|
||||
{
|
||||
xAxis.Minimum = DateTimeAxis.ToDouble(now.AddSeconds(-20));
|
||||
xAxis.Maximum = DateTimeAxis.ToDouble(now.AddSeconds(0.5));
|
||||
}
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 后台数据库批量入库
|
||||
private void StartDbFlushWorker(CancellationToken token)
|
||||
{
|
||||
_dbFlushTask = Task.Run(async () =>
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), token).ConfigureAwait(false);
|
||||
await DoFlushWorkAsync().ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[DB Worker Error] {ex.Message}");
|
||||
}
|
||||
}
|
||||
await DoFlushWorkAsync().ConfigureAwait(false);
|
||||
}, token);
|
||||
}
|
||||
|
||||
private async Task DoFlushWorkAsync()
|
||||
{
|
||||
if (_insertQueue.IsEmpty) return;
|
||||
if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
var listToInsert = new List<MonitorValueEntity>();
|
||||
while (_insertQueue.TryDequeue(out var entity)) listToInsert.Add(entity);
|
||||
|
||||
if (listToInsert.Count > 0)
|
||||
{
|
||||
var result = await _monitorValueService.InsertRangeAsync(listToInsert).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
finally { Interlocked.Exchange(ref _isFlushing, 0); }
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Navigation 导航
|
||||
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>();
|
||||
_systemConfig = _scope.Resolve<SystemConfig>();
|
||||
RaisePropertyChanged(nameof(Channels));
|
||||
_broadcaster = _scope.Resolve<HardwareDataBroadcaster>();
|
||||
_canSignalBroadcaster = _scope.Resolve<CANSignalBroadcaster>();
|
||||
IsInitiated = true;
|
||||
|
||||
if (_monitorCTS == null) _monitorCTS = new CancellationTokenSource();
|
||||
|
||||
// 1. 启动后台数据库消费
|
||||
StartDbFlushWorker(_monitorCTS.Token);
|
||||
|
||||
// 2. 扫描可监测方法(供 UI 选择)
|
||||
DiscoverAvailableMethods();
|
||||
|
||||
// 3. 从配置自动恢复已保存的监测通道
|
||||
RestoreChannelsFromConfig();
|
||||
|
||||
// 4. 启动广播器(Discover + Start)
|
||||
_broadcaster.Discover();
|
||||
_broadcaster.Start();
|
||||
_canSignalBroadcaster.Discover();
|
||||
_canSignalBroadcaster.Start();
|
||||
|
||||
// 5. 订阅 HardwareDataReportedEvent(UI 线程回调)
|
||||
_subscriptionToken = _eventAggregator.GetEvent<HardwareDataReportedEvent>()
|
||||
.Subscribe(OnHardwareDataReceived, ThreadOption.UIThread);
|
||||
|
||||
// 5b. 订阅 DBC 加载/卸载事件,动态发现或清除 CAN 信号
|
||||
_dbcLoadedToken = _eventAggregator.GetEvent<DBCLoadedEvent>()
|
||||
.Subscribe(OnDbcLoaded, ThreadOption.UIThread);
|
||||
_dbcUnloadedToken = _eventAggregator.GetEvent<DBCUnloadedEvent>()
|
||||
.Subscribe(OnDbcUnloaded, ThreadOption.UIThread);
|
||||
|
||||
// 5c. 根据 SystemConfig.ConfigurationList 刷新已加载 DBC 中的 CAN 信号
|
||||
RefreshConfiguredCanSignals();
|
||||
|
||||
// 6. 启动 OxyPlot 视觉刷新定时器
|
||||
_stopwatch.Start();
|
||||
_plotRefreshTimer.Start();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region PlotModel 构建
|
||||
private static PlotModel BuildEmptyPlot()
|
||||
{
|
||||
var pm = new PlotModel
|
||||
{
|
||||
Title = string.Empty,
|
||||
PlotAreaBorderColor = OxyColors.LightGray,
|
||||
Background = OxyColors.White
|
||||
};
|
||||
pm.Axes.Add(new DateTimeAxis
|
||||
{
|
||||
Position = AxisPosition.Bottom,
|
||||
Title = "时间",
|
||||
StringFormat = "HH:mm:ss",
|
||||
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 设备方法发现(仅用于 UI 展示可选项)
|
||||
/// <summary>
|
||||
/// 通过 GlobalInfo.HardwarePool 构建 设备实例引用→硬件指纹 的反向查找表。
|
||||
/// </summary>
|
||||
private Dictionary<object, string> BuildInstanceToFingerprint()
|
||||
{
|
||||
var map = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
{
|
||||
var lazy = poolEntry.Value;
|
||||
if (lazy.IsValueCreated && lazy.Value != null)
|
||||
map[lazy.Value] = poolEntry.Key;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private void DiscoverAvailableMethods()
|
||||
{
|
||||
AvailableMethods.Clear();
|
||||
if (_deviceManager?.DeviceMap == null || _deviceManager.DeviceMap.Count == 0)
|
||||
{
|
||||
StatusMessage = "当前工位无可用设备";
|
||||
return;
|
||||
}
|
||||
|
||||
var instanceToFp = BuildInstanceToFingerprint();
|
||||
|
||||
foreach (var kvp in _deviceManager.DeviceMap)
|
||||
{
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
var deviceType = device.GetType();
|
||||
|
||||
// 反查该实例的硬件指纹
|
||||
if (!instanceToFp.TryGetValue(device, out string? fingerprint))
|
||||
continue;
|
||||
|
||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
var parms = m.GetParameters();
|
||||
return parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
});
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||
? $"{deviceName}.{attr.Description}"
|
||||
: $"{deviceName}.{method.Name}";
|
||||
|
||||
AvailableMethods.Add(new AvailableMethodItem
|
||||
{
|
||||
DeviceName = deviceName,
|
||||
Fingerprint = fingerprint,
|
||||
MethodName = method.Name,
|
||||
DisplayName = displayName,
|
||||
MethodInfo = method,
|
||||
Device = device
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
StatusMessage = AvailableMethods.Count > 0
|
||||
? $"发现 {AvailableMethods.Count} 个可监测项"
|
||||
: "未发现可监测的设备方法";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 SystemConfig.MonitorChannels 持久化列表自动还原监测通道。
|
||||
/// </summary>
|
||||
private void RestoreChannelsFromConfig()
|
||||
{
|
||||
if (_systemConfig?.MonitorChannels == null || _systemConfig.MonitorChannels.Count == 0) return;
|
||||
|
||||
int restored = 0;
|
||||
foreach (var config in _systemConfig.MonitorChannels)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(config.Fingerprint) ||
|
||||
string.IsNullOrWhiteSpace(config.MethodName)) continue;
|
||||
|
||||
// 已在 Channels 中存在则跳过
|
||||
if (Channels.Any(c => c.Fingerprint == config.Fingerprint && c.MethodName == config.MethodName))
|
||||
continue;
|
||||
|
||||
// 在 AvailableMethods 中查找匹配项
|
||||
var method = AvailableMethods.FirstOrDefault(m =>
|
||||
m.Fingerprint == config.Fingerprint && m.MethodName == config.MethodName);
|
||||
if (method == null) continue;
|
||||
|
||||
var color = _palette[_colorIndex % _palette.Length];
|
||||
_colorIndex++;
|
||||
|
||||
var channel = new MonitorChannelVM
|
||||
{
|
||||
DeviceName = method.DeviceName,
|
||||
Fingerprint = method.Fingerprint,
|
||||
MethodName = method.MethodName,
|
||||
DisplayName = method.DisplayName,
|
||||
Color = color,
|
||||
IsMonitored = true,
|
||||
IsDisplayed = config.IsDisplayed
|
||||
};
|
||||
|
||||
// 仅当 IsDisplayed=true 时才创建 LineSeries
|
||||
if (channel.IsDisplayed)
|
||||
{
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
Color = color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
Plot.Series.Add(channel.Series);
|
||||
}
|
||||
|
||||
channel.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
|
||||
OnChannelDisplayChanged(channel);
|
||||
};
|
||||
|
||||
Channels.Add(channel);
|
||||
restored++;
|
||||
}
|
||||
|
||||
if (restored > 0)
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"已从配置自动恢复 {restored} 个监测通道";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 SystemConfig.ConfigurationList 刷新所有已加载 DBC 中的 CAN 信号。
|
||||
/// 只添加 ConfigurationList 中配置且实际存在于 DBC 的信号;不存在则清理。
|
||||
/// </summary>
|
||||
private void RefreshConfiguredCanSignals()
|
||||
{
|
||||
if (_systemConfig?.ConfigurationList == null || _systemConfig.ConfigurationList.Count == 0) return;
|
||||
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
|
||||
|
||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||
foreach (var channel in _systemConfig.ConfigurationList.Select(c => c.Channel).Distinct())
|
||||
{
|
||||
if (channel < 0 || channel >= msgDb.Count) continue;
|
||||
RefreshCanSignalsForChannel((uint)channel, msgDb[channel]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 刷新单个通道的 CAN 信号:以 ConfigurationList 为白名单,对照 DBC 实际存在性。
|
||||
/// - DBC 中存在 → 加入 AvailableMethods;若 MonitorChannels 中也有,则恢复 Channel。
|
||||
/// - DBC 中不存在 → 从 AvailableMethods 移除;若已在 Channels 中,则删除。
|
||||
/// </summary>
|
||||
private void RefreshCanSignalsForChannel(uint channel, List<_Msg_> messages)
|
||||
{
|
||||
if (_systemConfig?.ConfigurationList == null) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, channel);
|
||||
var configs = _systemConfig.ConfigurationList.Where(c => c.Channel == (int)channel).ToList();
|
||||
bool changed = false;
|
||||
|
||||
foreach (var cfg in configs)
|
||||
{
|
||||
if (string.IsNullOrEmpty(cfg.SignalName)) continue;
|
||||
|
||||
string methodName = CANSignalBroadcaster.BuildMethodName((uint)cfg.MessageID, cfg.SignalName);
|
||||
bool existsInDbc = messages.Any(m =>
|
||||
m.msg_id == cfg.MessageID &&
|
||||
m.signal_Name != null &&
|
||||
m.signal_Name.Contains(cfg.SignalName));
|
||||
|
||||
if (existsInDbc)
|
||||
{
|
||||
// 加入 AvailableMethods(去重)
|
||||
if (!AvailableMethods.Any(m => m.Fingerprint == fingerprint && m.MethodName == methodName))
|
||||
{
|
||||
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
||||
AvailableMethods.Add(new AvailableMethodItem
|
||||
{
|
||||
DeviceName = $"CAN{channel}",
|
||||
Fingerprint = fingerprint,
|
||||
MethodName = methodName,
|
||||
DisplayName = displayName,
|
||||
MethodInfo = null!,
|
||||
Device = null!
|
||||
});
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 如果 MonitorChannels 中有持久化记录,自动恢复 Channel
|
||||
var monitorConfig = _systemConfig.MonitorChannels?.FirstOrDefault(m =>
|
||||
m.Fingerprint == fingerprint && m.MethodName == methodName);
|
||||
if (monitorConfig != null && !Channels.Any(c =>
|
||||
c.Fingerprint == fingerprint && c.MethodName == methodName))
|
||||
{
|
||||
AddCanChannel(cfg, fingerprint, methodName, monitorConfig.IsDisplayed);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// DBC 中不存在:从 AvailableMethods 移除
|
||||
var methodToRemove = AvailableMethods.FirstOrDefault(m =>
|
||||
m.Fingerprint == fingerprint && m.MethodName == methodName);
|
||||
if (methodToRemove != null)
|
||||
{
|
||||
AvailableMethods.Remove(methodToRemove);
|
||||
changed = true;
|
||||
}
|
||||
|
||||
// 如果该信号已经在 Channels(或 MonitorChannels)中,删除
|
||||
var channelToRemove = Channels.FirstOrDefault(c =>
|
||||
c.Fingerprint == fingerprint && c.MethodName == methodName);
|
||||
if (channelToRemove != null)
|
||||
{
|
||||
if (channelToRemove.Series != null) Plot.Series.Remove(channelToRemove.Series);
|
||||
Channels.Remove(channelToRemove);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"CAN{channel} 已刷新配置信号";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCLoadedEvent 回调:DBC 加载后刷新该通道的配置信号。
|
||||
/// </summary>
|
||||
private void OnDbcLoaded(DBCLoadedArgs args)
|
||||
{
|
||||
if (args.Scope != TestStatus) return;
|
||||
if (_deviceManager?.CANFD?.DBCParser?.MsgDatabase == null) return;
|
||||
|
||||
int channel = (int)args.Channel;
|
||||
var msgDb = _deviceManager.CANFD.DBCParser.MsgDatabase;
|
||||
if (channel < 0 || channel >= msgDb.Count) return;
|
||||
|
||||
RefreshCanSignalsForChannel(args.Channel, msgDb[channel]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DBCUnloadedEvent 回调:移除对应通道的 CAN 信号(AvailableMethods + Channels)。
|
||||
/// </summary>
|
||||
private void OnDbcUnloaded(DBCUnloadedArgs args)
|
||||
{
|
||||
if (args.Scope != TestStatus) return;
|
||||
|
||||
string canDeviceFingerprint = _deviceManager?.GetCanDeviceFingerprint() ?? string.Empty;
|
||||
string fingerprint = CANSignalBroadcaster.BuildFingerprint(canDeviceFingerprint, args.Channel);
|
||||
|
||||
// 从 AvailableMethods 中移除
|
||||
var toRemoveMethods = AvailableMethods.Where(m => m.Fingerprint == fingerprint).ToList();
|
||||
foreach (var m in toRemoveMethods)
|
||||
AvailableMethods.Remove(m);
|
||||
|
||||
// 从 Channels 中移除(并清理 Plot)
|
||||
var toRemoveChannels = Channels.Where(c => c.Fingerprint == fingerprint).ToList();
|
||||
foreach (var c in toRemoveChannels)
|
||||
{
|
||||
if (c.Series != null) Plot.Series.Remove(c.Series);
|
||||
Channels.Remove(c);
|
||||
}
|
||||
|
||||
if (toRemoveMethods.Count > 0 || toRemoveChannels.Count > 0)
|
||||
{
|
||||
Plot.InvalidatePlot(true);
|
||||
StatusMessage = $"CAN{args.Channel} DBC 已卸载,移除 {toRemoveMethods.Count} 个信号";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将单个 CAN 信号添加为监测通道。
|
||||
/// </summary>
|
||||
private void AddCanChannel(CANSignalConfig cfg, string fingerprint, string methodName, bool isDisplayed)
|
||||
{
|
||||
var color = _palette[_colorIndex % _palette.Length];
|
||||
_colorIndex++;
|
||||
|
||||
string displayName = CANSignalBroadcaster.BuildDisplayName(cfg.MessageName, cfg.SignalName);
|
||||
var channel = new MonitorChannelVM
|
||||
{
|
||||
DeviceName = $"CAN{cfg.Channel}",
|
||||
Fingerprint = fingerprint,
|
||||
MethodName = methodName,
|
||||
DisplayName = displayName,
|
||||
Color = color,
|
||||
IsMonitored = true,
|
||||
IsDisplayed = isDisplayed
|
||||
};
|
||||
|
||||
if (channel.IsDisplayed)
|
||||
{
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
Color = color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
Plot.Series.Add(channel.Series);
|
||||
}
|
||||
|
||||
channel.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
|
||||
OnChannelDisplayChanged(channel);
|
||||
};
|
||||
|
||||
Channels.Add(channel);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令处理
|
||||
private void OnAddChannel()
|
||||
{
|
||||
var method = SelectedAvailableMethod;
|
||||
if (method == null)
|
||||
{
|
||||
StatusMessage = "请先在列表中选择一个监测项";
|
||||
return;
|
||||
}
|
||||
|
||||
// 以指纹+方法名去重,防止同一物理设备重复添加
|
||||
if (Channels.Any(c => c.Fingerprint == method.Fingerprint && c.MethodName == method.MethodName))
|
||||
{
|
||||
StatusMessage = $"监测项 [{method.DisplayName}] 已存在";
|
||||
return;
|
||||
}
|
||||
|
||||
var color = _palette[_colorIndex % _palette.Length];
|
||||
_colorIndex++;
|
||||
|
||||
var channel = new MonitorChannelVM
|
||||
{
|
||||
DeviceName = method.DeviceName,
|
||||
Fingerprint = method.Fingerprint,
|
||||
MethodName = method.MethodName,
|
||||
DisplayName = method.DisplayName,
|
||||
Color = color,
|
||||
IsMonitored = true,
|
||||
IsDisplayed = true
|
||||
};
|
||||
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
Color = color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
Plot.Series.Add(channel.Series);
|
||||
|
||||
channel.PropertyChanged += (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(MonitorChannelVM.IsDisplayed))
|
||||
OnChannelDisplayChanged(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);
|
||||
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 显示/隐藏切换与数学变换
|
||||
public void OnChannelDisplayChanged(MonitorChannelVM channel)
|
||||
{
|
||||
if (channel.IsDisplayed)
|
||||
{
|
||||
if (channel.Series == null)
|
||||
{
|
||||
channel.Series = new LineSeries
|
||||
{
|
||||
Title = channel.DisplayName,
|
||||
Color = channel.Color,
|
||||
StrokeThickness = 1.5
|
||||
};
|
||||
|
||||
var recent = channel.DataPoints.ToArray();
|
||||
var startIdx = Math.Max(0, recent.Length - 10000);
|
||||
for (int i = startIdx; i < recent.Length; i++)
|
||||
channel.Series.Points.Add(new DataPoint(recent[i].Time.ToOADate(), recent[i].DisplayValue));
|
||||
|
||||
Plot.Series.Add(channel.Series);
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (channel.Series != null)
|
||||
{
|
||||
Plot.Series.Remove(channel.Series);
|
||||
channel.Series = null;
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnChannelMathChanged(MonitorChannelVM channel)
|
||||
{
|
||||
if (channel.Series == null) return;
|
||||
var points = channel.DataPoints.ToArray();
|
||||
channel.Series.Points.Clear();
|
||||
var startIdx = Math.Max(0, points.Length - 10000);
|
||||
for (int i = startIdx; i < points.Length; i++)
|
||||
channel.Series.Points.Add(new DataPoint(points[i].Time.ToOADate(), points[i].DisplayValue));
|
||||
Plot.InvalidatePlot(true);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
410
MonitorModule/ViewModels/RecordViewModel.cs
Normal file
410
MonitorModule/ViewModels/RecordViewModel.cs
Normal file
@@ -0,0 +1,410 @@
|
||||
using Common.Attributes;
|
||||
using DeviceCommand.Base;
|
||||
using Logger;
|
||||
using Model.Entity;
|
||||
using ORM;
|
||||
using SqlSugar;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Windows.Input;
|
||||
using UIShare.GlobalVariable;
|
||||
using UIShare.PubEvent;
|
||||
using UIShare.ViewModelBase;
|
||||
|
||||
namespace MonitorModule.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 记录界面:查询 MonitorValueEntity 表中的历史监测数据。
|
||||
/// 功能:监测项筛选 / 台架筛选 / 日期筛选 / 分页 / 导出 CSV。
|
||||
/// 使用 SqlSugarContext.DbContext 全局单例,与写入端共用同一数据库连接。
|
||||
/// </summary>
|
||||
public class RecordViewModel : NavigateViewModelBase, IRegionMemberLifetime, IDisposable
|
||||
{
|
||||
|
||||
#region 属性
|
||||
public bool KeepAlive => true;
|
||||
public ScopedContext _scopedContext { get; set; }
|
||||
public GlobalInfo _globalInfo { get; }
|
||||
|
||||
public string TestStatus
|
||||
{
|
||||
get => _testStatus;
|
||||
set => SetProperty(ref _testStatus, value);
|
||||
}
|
||||
|
||||
/// <summary>监测项名称下拉列表</summary>
|
||||
public ObservableCollection<string> MonitorNames
|
||||
{
|
||||
get => _monitorNames;
|
||||
set => SetProperty(ref _monitorNames, value);
|
||||
}
|
||||
|
||||
/// <summary>选中的监测项(null = 全部)</summary>
|
||||
public string? SelectedMonitorName
|
||||
{
|
||||
get => _selectedMonitorName;
|
||||
set => SetProperty(ref _selectedMonitorName, value);
|
||||
}
|
||||
|
||||
/// <summary>台架(作用域)下拉列表</summary>
|
||||
public ObservableCollection<string> ScopeNames
|
||||
{
|
||||
get => _scopeNames;
|
||||
set => SetProperty(ref _scopeNames, value);
|
||||
}
|
||||
|
||||
/// <summary>选中的台架(null = 全部)</summary>
|
||||
public string? SelectedScope
|
||||
{
|
||||
get => _selectedScope;
|
||||
set => SetProperty(ref _selectedScope, value);
|
||||
}
|
||||
|
||||
/// <summary>查询日期(null = 全部日期)</summary>
|
||||
public DateTime? SelectedStartDate
|
||||
{
|
||||
get => _selectedStartDate;
|
||||
set => SetProperty(ref _selectedStartDate, value);
|
||||
}
|
||||
public DateTime? SelectedEndDate
|
||||
{
|
||||
get => _selectedEndDate;
|
||||
set => SetProperty(ref _selectedEndDate, value);
|
||||
}
|
||||
|
||||
public DataTable ResultTable
|
||||
{
|
||||
get => _resultTable;
|
||||
set => SetProperty(ref _resultTable, value);
|
||||
}
|
||||
|
||||
public string StatusMessage
|
||||
{
|
||||
get => _statusMessage;
|
||||
set => SetProperty(ref _statusMessage, value);
|
||||
}
|
||||
|
||||
public int PageIndex
|
||||
{
|
||||
get => _pageIndex;
|
||||
set => SetProperty(ref _pageIndex, value);
|
||||
}
|
||||
|
||||
public int PageSize
|
||||
{
|
||||
get => _pageSize;
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _pageSize, value <= 0 ? 50 : value))
|
||||
{
|
||||
RaisePropertyChanged(nameof(TotalPages));
|
||||
PageIndex = 1;
|
||||
Query();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long TotalCount
|
||||
{
|
||||
get => _totalCount;
|
||||
set
|
||||
{
|
||||
SetProperty(ref _totalCount, value);
|
||||
RaisePropertyChanged(nameof(TotalPages));
|
||||
}
|
||||
}
|
||||
|
||||
public int TotalPages
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PageSize <= 0) return 1;
|
||||
var pages = (int)((TotalCount + PageSize - 1) / PageSize);
|
||||
return pages <= 0 ? 1 : pages;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 命令
|
||||
public ICommand LoadedCommand { get; }
|
||||
public ICommand QueryCommand { get; }
|
||||
public ICommand ClearFilterCommand { get; }
|
||||
public ICommand FirstPageCommand { get; }
|
||||
public ICommand PrevPageCommand { get; }
|
||||
public ICommand NextPageCommand { get; }
|
||||
public ICommand LastPageCommand { get; }
|
||||
public ICommand ExportCsvCommand { get; }
|
||||
public ICommand RefreshCommand { get; }
|
||||
#endregion
|
||||
|
||||
#region 私有字段
|
||||
private IScopedProvider _scope;
|
||||
private string _testStatus = string.Empty;
|
||||
private ObservableCollection<string> _monitorNames = new();
|
||||
private string? _selectedMonitorName;
|
||||
private ObservableCollection<string> _scopeNames = new(new[] { "全部", "TestCell1", "TestCell2", "TestCell3", "TestCell4", "TestCell5", "TestCell6", "TestCell7", "TestCell8" });
|
||||
private string? _selectedScope = "全部";
|
||||
private DateTime? _selectedStartDate;
|
||||
private DateTime? _selectedEndDate;
|
||||
private DataTable _resultTable = new();
|
||||
private string _statusMessage = "未连接";
|
||||
private int _pageIndex = 1;
|
||||
private int _pageSize = 50;
|
||||
private long _totalCount;
|
||||
private bool IsInitiated = false;
|
||||
#endregion
|
||||
|
||||
public RecordViewModel(IContainerExtension container) : base(container)
|
||||
{
|
||||
_globalInfo = container.Resolve<GlobalInfo>();
|
||||
LoadedCommand = new DelegateCommand(LoadFilterOptions);
|
||||
QueryCommand = new DelegateCommand(() => { PageIndex = 1; Query(); });
|
||||
ClearFilterCommand = new DelegateCommand(ClearFilter);
|
||||
FirstPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex = 1; Query(); } });
|
||||
PrevPageCommand = new DelegateCommand(() => { if (PageIndex > 1) { PageIndex--; Query(); } });
|
||||
NextPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex++; Query(); } });
|
||||
LastPageCommand = new DelegateCommand(() => { if (PageIndex < TotalPages) { PageIndex = TotalPages; Query(); } });
|
||||
ExportCsvCommand = new DelegateCommand(ExportCsv);
|
||||
RefreshCommand = new DelegateCommand(OnExpand);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scope?.Dispose();
|
||||
}
|
||||
|
||||
#region 筛选条件加载
|
||||
/// <summary>
|
||||
/// 加载筛选条件:监测项通过反射发现(与 MonitorViewModel 一致),台架从数据库查询。
|
||||
/// </summary>
|
||||
private void LoadFilterOptions()
|
||||
{
|
||||
// 监测项:反射发现当前作用域所有带 [Monitorable] 的设备方法
|
||||
DiscoverMonitorNames();
|
||||
|
||||
Query();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 反射扫描当前作用域 DeviceManager 中所有带 [Monitorable] 特性的设备方法,
|
||||
/// 生成与 MonitorViewModel 完全一致的 DisplayName 列表,填充到下拉框。
|
||||
/// 这样即使数据库还没数据,用户也能看到所有可筛选的监测项。
|
||||
/// </summary>
|
||||
private void DiscoverMonitorNames()
|
||||
{
|
||||
MonitorNames = new ObservableCollection<string>();
|
||||
|
||||
if (_scope == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var deviceManager = _scope.Resolve<DeviceManager>();
|
||||
if (deviceManager?.DeviceMap == null || deviceManager.DeviceMap.Count == 0)
|
||||
{
|
||||
StatusMessage = "当前工位无可用设备";
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建 设备实例 → 硬件指纹 的反向查找表
|
||||
var instanceToFp = new Dictionary<object, string>(ReferenceEqualityComparer.Instance);
|
||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||
{
|
||||
var lazy = poolEntry.Value;
|
||||
if (lazy.IsValueCreated && lazy.Value != null)
|
||||
instanceToFp[lazy.Value] = poolEntry.Key;
|
||||
}
|
||||
|
||||
var names = new List<string>();
|
||||
|
||||
foreach (var kvp in deviceManager.DeviceMap)
|
||||
{
|
||||
string deviceName = kvp.Key;
|
||||
var device = kvp.Value;
|
||||
var deviceType = device.GetType();
|
||||
|
||||
// 只列出在硬件指纹池中注册的设备
|
||||
if (!instanceToFp.ContainsKey(device))
|
||||
continue;
|
||||
|
||||
var methods = deviceType.GetMethods(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(m =>
|
||||
{
|
||||
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
|
||||
if (m.ReturnType != typeof(Task<string>)) return false;
|
||||
var parms = m.GetParameters();
|
||||
return parms.Length == 0 ||
|
||||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
|
||||
});
|
||||
|
||||
foreach (var method in methods)
|
||||
{
|
||||
var attr = method.GetCustomAttribute<MonitorableAttribute>();
|
||||
string displayName = !string.IsNullOrEmpty(attr?.Description)
|
||||
? $"{deviceName}.{attr.Description}"
|
||||
: $"{deviceName}.{method.Name}";
|
||||
names.Add(displayName);
|
||||
}
|
||||
}
|
||||
|
||||
names.Sort();
|
||||
MonitorNames = new ObservableCollection<string>(names);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(_globalInfo.CurrentScope, $"反射发现监测项失败:{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>清除所有筛选条件</summary>
|
||||
private void ClearFilter()
|
||||
{
|
||||
SelectedMonitorName = null;
|
||||
SelectedScope = "全部";
|
||||
SelectedStartDate = null;
|
||||
SelectedEndDate = null;
|
||||
PageIndex = 1;
|
||||
Query();
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 查询
|
||||
/// <summary>
|
||||
/// 按当前筛选条件分页查询 MonitorValueEntity。
|
||||
/// </summary>
|
||||
private void Query()
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
|
||||
// 预计算日期范围,避免在表达式树中访问可空属性
|
||||
DateTime? dateStart = SelectedStartDate?.Date;
|
||||
DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
|
||||
|
||||
var query = db.Queryable<MonitorValueEntity>()
|
||||
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
|
||||
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
|
||||
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc);
|
||||
|
||||
TotalCount = query.Count();
|
||||
|
||||
// 修正越界
|
||||
if (PageIndex > TotalPages) PageIndex = TotalPages;
|
||||
if (PageIndex < 1) PageIndex = 1;
|
||||
|
||||
int skip = (PageIndex - 1) * PageSize;
|
||||
ResultTable = query
|
||||
.Skip(skip)
|
||||
.Take(PageSize)
|
||||
.ToDataTable();
|
||||
|
||||
StatusMessage = $"共 {TotalCount} 行 第 {PageIndex}/{TotalPages} 页";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(TestStatus, $"查询失败:{ex.Message}");
|
||||
StatusMessage = $"查询失败:{ex.Message}";
|
||||
ResultTable = new DataTable();
|
||||
TotalCount = 0;
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 导出 CSV
|
||||
/// <summary>
|
||||
/// 按当前筛选条件导出全部结果到 CSV(不分页)。
|
||||
/// </summary>
|
||||
private void ExportCsv()
|
||||
{
|
||||
try
|
||||
{
|
||||
var db = SqlSugarContext.DbContext;
|
||||
|
||||
DateTime? dateStart = SelectedStartDate?.Date;
|
||||
DateTime? dateEnd = SelectedEndDate?.Date.AddDays(1);
|
||||
|
||||
var query = db.Queryable<MonitorValueEntity>()
|
||||
.WhereIF(!string.IsNullOrEmpty(SelectedMonitorName), x => x.MonitorName == SelectedMonitorName)
|
||||
.WhereIF(!string.IsNullOrEmpty(SelectedScope) && SelectedScope != "全部", x => x.Scope == SelectedScope)
|
||||
.WhereIF(dateStart.HasValue, x => x.CreateTime >= dateStart && x.CreateTime < dateEnd!.Value)
|
||||
.OrderBy(x => x.CreateTime, OrderByType.Desc);
|
||||
|
||||
var dt = query.ToDataTable();
|
||||
if (dt.Rows.Count == 0)
|
||||
{
|
||||
StatusMessage = "无可导出数据";
|
||||
return;
|
||||
}
|
||||
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Filter = "CSV 文件 (*.csv)|*.csv|所有文件|*.*",
|
||||
FileName = $"MonitorData_{DateTime.Now:yyyyMMdd_HHmmss}.csv",
|
||||
Title = "导出监测数据为 CSV"
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
WriteCsv(dlg.FileName, dt);
|
||||
|
||||
StatusMessage = $"导出完成:{dlg.FileName}({dt.Rows.Count} 行)";
|
||||
LoggerHelper.InfoWithNotify(TestStatus, $"工位 [{TestStatus}] 导出监测数据至 {dlg.FileName},共 {dt.Rows.Count} 行");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.ErrorWithNotify(TestStatus, $"导出失败:{ex.Message}");
|
||||
StatusMessage = $"导出失败:{ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteCsv(string path, DataTable dt)
|
||||
{
|
||||
// 写 UTF-8 BOM 让 Excel 直接识别中文
|
||||
using var sw = new StreamWriter(path, false, new UTF8Encoding(true));
|
||||
// 表头
|
||||
sw.WriteLine(string.Join(",", dt.Columns.Cast<DataColumn>().Select(c => Escape(c.ColumnName))));
|
||||
// 行
|
||||
foreach (DataRow row in dt.Rows)
|
||||
{
|
||||
sw.WriteLine(string.Join(",", row.ItemArray.Select(v => Escape(v?.ToString() ?? string.Empty))));
|
||||
}
|
||||
}
|
||||
|
||||
private static string Escape(string field)
|
||||
{
|
||||
if (field.Contains('"') || field.Contains(',') || field.Contains('\r') || field.Contains('\n'))
|
||||
{
|
||||
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
return field;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 双击展开
|
||||
private void OnExpand()
|
||||
{
|
||||
if (string.IsNullOrEmpty(TestStatus)) return;
|
||||
_globalInfo.CurrentScope = TestStatus;
|
||||
_eventAggregator.GetEvent<ExpandViewEvent>().Publish(TestStatus);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region 重写
|
||||
public override void OnNavigatedTo(NavigationContext navigationContext)
|
||||
{
|
||||
base.OnNavigatedTo(navigationContext);
|
||||
if (!IsInitiated && navigationContext.Parameters.ContainsKey("Name"))
|
||||
{
|
||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||
_scope = _globalInfo.ScopeDic[TestStatus];
|
||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||
IsInitiated = true;
|
||||
}
|
||||
LoadFilterOptions();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user