using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Data; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Forms; using Aitex.Core.RT.Log; using Aitex.Sorter.Common; using MECF.Framework.Common.DataCenter; using MECF.Framework.Common.Equipment; using MECF.Framework.UI.Client.CenterViews.DataLogs.Event; using MECF.Framework.UI.Client.CenterViews.DataLogs.ProcessHistory; #if !EXPORT_TO_CSV using MECF.Framework.Common.Utilities; #endif using MECF.Framework.UI.Client.CenterViews.Operations.RealTime; using MECF.Framework.UI.Client.ClientBase; using MECF.Framework.UI.Client.ClientBase.Charting; using MECF.Framework.UI.Client.ClientBase.EventArgs; using MECF.Framework.UI.Client.ClientBase.Pipelines; using MECF.Framework.UI.Client.ClientBase.Tree; using SciChart.Charting.Model.DataSeries; using SciChart.Charting.Visuals.Axes; using SciChart.Charting.Visuals.RenderableSeries; using SciChart.Core.Extensions; using SciChart.Core.Framework; using SciChart.Data.Model; using Cali = Caliburn.Micro; using DataGridCell = System.Windows.Controls.DataGridCell; using DateRange = SciChart.Data.Model.DateRange; using MessageBox = System.Windows.MessageBox; using TreeNode = MECF.Framework.UI.Client.ClientBase.Tree.TreeNode; namespace MECF.Framework.UI.Client.CenterViews.DataLogs.DataHistory { public class DataViewModel : BusyIndicateableUiViewModelBase { #region Variables /// /// 一次最多查询的项目数。 /// private const int MAX_ITEMS_PER_QUERY = 50; private IRange _timeRange; private IRange _visibleRangeValue; private AutoRange _autoRange; private CancellationTokenSource _cancellationTokenSource; /// /// 更新报告导出信息。 /// private readonly IProgress _progCsvExport; /// /// 查询进度信息更新。 /// private IProgress _progQueryUpdate; /// /// 挂起或恢复Chart更新 /// private readonly IProgress _progChartSuspendUpdating; private IUpdateSuspender _suspender; /// /// 打开错误消息对话框。 /// private readonly IProgress _progShowErrorMessageBox; #endregion #region Constructors public DataViewModel() { DisplayName = "Data History"; SelectedData = new ChartingLineSeriesCollection(DisplayName); var now = DateTime.Today; SearchBeginTime = DateTime.Now.AddHours(-1); // -new TimeSpan(1, 0, 0, 0); SearchEndTime = DateTime.Now; //SearchBeginTime = new DateTime(now.Year, now.Month, now.Day, 00, 00, 00); ;// -new TimeSpan(1, 0, 0, 0); //SearchEndTime = new DateTime(now.Year, now.Month, now.Day, 23, 59, 59); var provider = new RealtimeProvider(); ParameterNodes = new TreeNode(DisplayName) { MaxTerminalSelectionAllowed = MAX_ITEMS_PER_QUERY }; ParameterNodes.ChildNodes.AddRange(provider.GetTreeNodeParameters()); VisibleRangeTime = new DateRange(DateTime.Now.AddMinutes(-60), DateTime.Now.AddMinutes(60)); VisibleRangeValue = new DoubleRange(0, 10); _progQueryUpdate = new Progress(e => { if (e.CurrentProgress == e.TotalProgress) { IsBusy = false; if (_cancellationTokenSource?.Token.IsCancellationRequested == false) { foreach (var renderableSeries in SelectedData) { var series = renderableSeries as FastLineSeries; var dataSeries = series?.GetDataSeries(); try { if (series != null && dataSeries != null) { var node = series.BackendParameterNode; if (double.IsInfinity((double)dataSeries.YRange.Diff)) { node.ClearStatistic(); } else { var min = ((double)dataSeries.YMin); var max = ((double)dataSeries.YMax); var average = dataSeries.Metadata.Cast().Average(x => x.Value); node.SetStatistic(min, max, average); } } } catch (Exception ex) { var err = $"It's failed to load data of {series?.DataName ?? "Unknown"}, {ex.Message}"; LOG.Error(err, ex); } } } ChartAutoRange = AutoRange.Never; ((DataView)View).chart.ZoomExtents(); } BusyIndicatorContent = e.Message; }); _progCsvExport = new Progress(e => { BusyIndicatorContent = e.Message; }); _progChartSuspendUpdating = new Progress(isSuspend => { if (isSuspend) _suspender = ((DataView)View).chart.SuspendSuspendUpdates(); else { try { if (_suspender?.IsSuspended == true) _suspender?.SafeDispose(); } catch (Exception) { // 查询过程最后会强制恢复挂起一次,可能引发异常,忽略此异常。 } } }); _progShowErrorMessageBox = new Progress((error => { System.Windows.Forms.MessageBox.Show(error, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); })); } #endregion #region Property public bool IsPermission => Permission == 3; private DataView view; public TreeNode ParameterNodes { get; } public ChartingLineSeriesCollection SelectedData { get; set; } public AutoRange ChartAutoRange { get => _autoRange; set { _autoRange = value; NotifyOfPropertyChange(nameof(ChartAutoRange)); } } public IRange VisibleRangeTime { get => _timeRange; set { _timeRange = value; NotifyOfPropertyChange(nameof(VisibleRangeTime)); } } public IRange VisibleRangeValue { get => _visibleRangeValue; set { _visibleRangeValue = value; NotifyOfPropertyChange(nameof(VisibleRangeValue)); } } //public DateTime StartDateTime //{ // get => ((DataView)View).wfTimeFrom.Value; // set // { // ((DataView)View).wfTimeFrom.Value = value; // NotifyOfPropertyChange(nameof(StartDateTime)); // } //} //public DateTime EndDateTime //{ // get => ((DataView)View).wfTimeTo.Value; // set // { // ((DataView)View).wfTimeTo.Value = value; // NotifyOfPropertyChange(nameof(EndDateTime)); // } //} public DateTime SearchBeginTime { get; set; } public DateTime SearchEndTime { get; set; } #endregion #region Methods protected override void OnViewLoaded(object view) { base.OnViewLoaded(view); this.view = (DataView)view; //StartDateTime = DateTime.Now.Date; //EndDateTime = DateTime.Now.Date.AddDays(1).AddTicks(-1); this.view.wfTimeFrom.Content = SearchBeginTime.ToString("yyyy-MM-dd HH:mm:ss"); this.view.wfTimeTo.Content = SearchEndTime.ToString("yyyy-MM-dd HH:mm:ss"); } public void SelectDate(string SelectType) { var windowManager = Caliburn.Micro.Core.IoC.Get(); if (SelectType == "Start") { this.SearchBeginTime = Convert.ToDateTime(this.view.wfTimeFrom.Content); SelectDateViewModel selectdateViewModel = new SelectDateViewModel(SearchBeginTime); var result = (windowManager as Caliburn.Micro.WindowManager)?.ShowDialogWithTitle(selectdateViewModel, null, "Start Time"); if (result == true) { SearchBeginTime = selectdateViewModel.SearchDate; this.view.wfTimeFrom.Content = selectdateViewModel.SearchDate; } } else { this.SearchEndTime = Convert.ToDateTime(this.view.wfTimeTo.Content); SelectDateViewModel selectdateViewModel = new SelectDateViewModel(SearchEndTime); var result = (windowManager as Caliburn.Micro.WindowManager)?.ShowDialogWithTitle(selectdateViewModel, null, "End Time"); if (result == true) { this.view.wfTimeTo.Content = selectdateViewModel.SearchDate; } } } public void ZoomInClick(int index) { var view = GetView() as DataView; index += 1; if ((index & 0x01) == 1) view.chart.sciChart.ChartModifier.XAxis.ZoomBy(-0.1, -0.1); if ((index & 0x02) == 2) view.chart.sciChart.ChartModifier.YAxis.ZoomBy(-0.1, -0.1); } public void ZoomOutClick(int index) { var view = GetView() as DataView; index += 1; if ((index & 0x01) == 1) view.chart.sciChart.ChartModifier.XAxis.ZoomBy(0.1, 0.1); if ((index & 0x02) == 2) view.chart.sciChart.ChartModifier.YAxis.ZoomBy(0.1, 0.1); } public void ArrowClick(string direction) { var view = GetView() as DataView; switch (direction) { case "Up": view.chart.sciChart.ChartModifier.YAxis.Scroll(50, SciChart.Charting.ClipMode.ClipAtMax); break; case "Down": view.chart.sciChart.ChartModifier.YAxis.Scroll(-50, SciChart.Charting.ClipMode.ClipAtMin); break; case "Left": view.chart.sciChart.ChartModifier.XAxis.Scroll(-50, SciChart.Charting.ClipMode.ClipAtMin); break; case "Right": view.chart.sciChart.ChartModifier.XAxis.Scroll(50, SciChart.Charting.ClipMode.ClipAtMax); break; } } public void Query(object parameter) { WaferHistoryRecipe waferHistoryRecipe = parameter as WaferHistoryRecipe; if (waferHistoryRecipe != null) { System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => { if (waferHistoryRecipe.StartTime != DateTime.MinValue) SearchBeginTime = waferHistoryRecipe.StartTime; if (waferHistoryRecipe.EndTime != DateTime.MinValue) SearchEndTime = waferHistoryRecipe.EndTime; else SearchEndTime = waferHistoryRecipe.StartTime.AddHours(1); //ParameterNodes.ForEachDo((x) => //{ // x.Selected = true; // ParameterCheck(x); //}); QueryData(); })); } } /// /// 查询数据。 /// /// public void QueryData(bool isAppend = false) { this.SearchBeginTime = Convert.ToDateTime(this.view.wfTimeFrom.Content); this.SearchEndTime = Convert.ToDateTime(this.view.wfTimeTo.Content); if (SearchBeginTime > SearchEndTime) { MessageBox.Show("time range invalid, start time should be early than end time.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); return; } // 哪些模组(TreeView的顶层Nodes)有选中项。 var selectedModules = ParameterNodes.ChildNodes.Where(x => x.HasTerminalSelected).ToList(); if (!selectedModules.Any()) { MessageBox.Show($"No item(s) are selected to query.", "Warning", MessageBoxButton.OK, MessageBoxImage.Warning); return; } VisibleRangeTime = new DateRange(SearchBeginTime.AddHours(-1), SearchEndTime.AddHours(1)); ChartAutoRange = AutoRange.Always; BusyIndicatorContent = "Preparing list ..."; IsBusy = true; _cancellationTokenSource = new CancellationTokenSource(); #region 生成曲线列表 var selectedTerminal = selectedModules.SelectMany(x => x.Flatten(true).Where(p => p.IsSelected == true)).ToList(); List appendedSeries = null; if (isAppend) appendedSeries = SelectedData.Append(selectedTerminal); else { SelectedData.ReArrange(selectedTerminal); appendedSeries = SelectedData.ToList(); } SelectedData.ResetColors(); #endregion var dataSeriesList = appendedSeries.Select(x => x.DataSeries).ToList(); Task.Run(async () => { // 延时启动,等待UI准备好 await Task.Delay(500); var pipeline = new TwoStagePipelineBasedTaskExecutor(); pipeline.Stage2ActionStarted += (s, e) => { // 挂起Chart _progChartSuspendUpdating.Report(true); }; pipeline.Stage2ActionFinished += (s, e) => { // 挂起刷新 _progChartSuspendUpdating.Report(false); }; pipeline.Stage1Finished += (s, e) => { // 查询完毕,Chart可能仍在渲染。 _progQueryUpdate?.Report(new ProgressUpdatingEventArgs(50, 100, "Still rendering chart ...")); }; using (pipeline) { var plTasks = pipeline.Start(null); try { //! 按时间分段查询,解决查询速度慢导致卡后台业务的问题。 //var ts = SearchEndTime - SearchBeginTime; //if (ts.Days <= 1) //{ // Query(pipeline, selectedModules, dataSeriesList, view.dataGrid1, SearchBeginTime, SearchEndTime, // _cancellationTokenSource, _progQueryUpdate); // // 结束流水线 // pipeline.AppendFunc1(null); // pipeline.AppendFunc2(null); //} //else { var daySlices = DateRangeHelper.SplitInToHours(new DateRangeHelper(SearchBeginTime, SearchEndTime), 12); foreach (var range in daySlices) { Query(pipeline, selectedModules, dataSeriesList, view.dataGrid1, range.Start, range.End, _cancellationTokenSource, _progQueryUpdate); if (_cancellationTokenSource.Token.IsCancellationRequested) break; await Task.Delay(1); } // 结束流水线 pipeline.AppendFunc1(null); pipeline.AppendFunc2(null); } await Task.WhenAll(plTasks.ToArray()); } catch (ThreadAbortException ex) { Thread.Sleep(500); // 查询操作被取消。 Debug.WriteLine(ex); } catch (AggregateException ae) { var errs = new StringBuilder(); foreach (var ex in ae.Flatten().InnerExceptions) { LOG.Error(ex.Message, ex); errs.AppendLine(ex.Message); } var errMsg = $"It's failed to query data, {errs}"; _progShowErrorMessageBox.Report(errMsg); LOG.Error(errMsg, ae); } catch (Exception ex) { var errMsg = $"It's failed to query data, {ex.Message}"; _progShowErrorMessageBox.Report(errMsg); LOG.Error(errMsg, ex); } finally { // 等待一下UI Thread.Sleep(100); _progQueryUpdate.Report(new ProgressUpdatingEventArgs(100, 100, "")); // 强制恢复Chart刷新,避免异常情况导致的Chart挂起。 _progChartSuspendUpdating.Report(false); } } }); } /// /// 查询数据 /// private static void Query(TwoStagePipelineBasedTaskExecutor pipeline, IEnumerable selectedModules, List dataSeriesList, System.Windows.Controls.DataGrid dataGrid, DateTime startTime, DateTime endTime, CancellationTokenSource cancellation, IProgress progressReporter = null) { pipeline.AppendFunc1(() => SearchDataBaseAsync( selectedModules, new DateRangeHelper(startTime, endTime), cancellation, progressReporter)); pipeline.AppendFunc2(ds => { RenderChartAndTable(ds, dataSeriesList, dataGrid, cancellation, progressReporter); return null; }); //pipeline.AppendFunc2(ds => //{ // RenderDataGridTable(ds, , cancellation, progressReporter); // return null; //}); } /// /// 取消查询。 /// public void CancelQuery() { Task.Run(() => { if (_cancellationTokenSource?.Token.CanBeCanceled == true) { _cancellationTokenSource.Cancel(); } Thread.Sleep(100); _progQueryUpdate.Report(new ProgressUpdatingEventArgs(100, 100, "")); _progChartSuspendUpdating.Report(false); }); if (View is DataView view) view.dataGrid.CancelOperation(); } /// /// 检查指定的表名是否存在。 /// /// /// private static bool CheckTableExists(string tableName) { var sql = $"SELECT EXISTS ( SELECT FROM pg_tables WHERE schemaname = 'public' AND tablename = '{tableName}' );"; var table = QueryDataClient.Instance.Service.QueryData(sql); if (table == null) return false; if (table.Rows.Count <= 0) return false; var value = table.Rows[0][0].ToString(); if (value.ToLower() == "true") return true; return false; } /// /// 根据左侧选项查询数据 /// /// private static DataSet SearchDataBaseAsync( IEnumerable modules, DateRangeHelper dateRange, CancellationTokenSource cancellation = null, IProgress progressReporter = null) { var ds = new DataSet(); using (cancellation?.Token.Register(Thread.CurrentThread.Abort, true)) { // 遍历模组 foreach (var module in modules) { var sql = new StringBuilder(); //! 因为数据库中按天拆表,无法一次性查询数据,需使用UNION合并多表查询,因此此处按天拼接SQL表达式 // 最终SQL表达式结构为: // (select xx from date1.xx) union (select xx from date2.xx) union (select xx from date3.xx) // where time between xxx and xxx // order by time asc var ts = dateRange.Diff; for (var day = 0; day <= ts.Days; day++) { // 检查表名是否存在,否则SQL执行出错。 var tblName = $"{dateRange.Start.AddDays(day):yyyyMMdd}.{module}"; if (module.ToString() == "IO") { //var node = module.ChildNodes.FirstOrDefault(x => (bool)x.HasTerminalSelected); tblName = $"{dateRange.Start.AddDays(day):yyyyMMdd}.System"; } else if (!ModuleHelper.IsPm(module.ToString())) { tblName = $"{dateRange.Start.AddDays(day):yyyyMMdd}.System"; } if (CheckTableExists(tblName)) { sql.Append("select \"time\" AS InternalTimeStamp"); var selectedParams = module.Flatten(true) .Where(x => x.IsSelected == true); // 添加待查询的列 foreach (var item in selectedParams) { sql.Append("," + $"\"{item}\""); } sql.Append($" from \"{tblName}\" "); if (day < ts.Days) sql.Append(" UNION "); } } // 所有表名不可用,可能是日期范围错误 if (sql.Length <= 0) { continue; } sql.Append( $" where \"time\" between {dateRange.Start.Ticks} and {dateRange.End.Ticks} order by InternalTimeStamp asc"); progressReporter?.Report(new ProgressUpdatingEventArgs(20, 100, $"Querying {dateRange}...")); if (cancellation?.Token.IsCancellationRequested == true) return null; // 查询数据并将返回的结果存储在DataSet中 var dataTable = QueryDataClient.Instance.Service.QueryData(sql.ToString()); if (cancellation?.Token.IsCancellationRequested == true) return null; //! 返回的 DataTable 可能不存在,原因是上述代码自动生成的表明可能不存在。 if (dataTable == null) continue; dataTable.TableName = module.Name; ds.Tables.Add(dataTable); } } return ds; } /// /// 渲染图表。 /// /// /// /// /// /// private static void RenderChartAndTable( DataSet ds, List dataSeriesList, System.Windows.Controls.DataGrid dataGrid, CancellationTokenSource cancellation = null, IProgress progressReporter = null) { if (ds == null || ds.Tables.Count <= 0) return; Dictionary> bigTable = new Dictionary>(); System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => { dataGrid.Columns.Clear(); DataGridTextColumn dataGridTextColumn = new DataGridTextColumn(); dataGridTextColumn.Header = "Time"; dataGridTextColumn.Binding = new System.Windows.Data.Binding("internaltimestamp"); ((System.Windows.Data.Binding)dataGridTextColumn.Binding).Converter = new StringToDateTimeStringConvert(); dataGrid.Columns.Add(dataGridTextColumn); })); // 一个Table一个模组 foreach (var table in ds.Tables.Cast()) { if (table.Rows.Count <= 0) continue; // 一列对应模组中的某个数据记录点 foreach (var col in table.Columns.Cast()) { // 忽略时间列 if (col.Ordinal == 0) continue; Dictionary dictColumn = new Dictionary(); var fullName = col.ColumnName; col.ColumnName = col.ColumnName.Replace(".", ""); var dataSeries = dataSeriesList.FirstOrDefault(x => x.SeriesName == fullName) as XyDataSeries; if (dataSeries == null) continue; var rows = table.Rows; var dateList = new List(); var valueList = new List(); var metaList = new List(); for (var i = 0; i < rows.Count; i++) { var date = new DateTime(long.Parse(rows[i][0].ToString())); var cellValue = rows[i][col]; var value = double.NaN; if (cellValue is bool b) value = b ? 1 : 0; else if (double.TryParse(cellValue.ToString(), out var num)) value = num; else value = 0; dateList.Add(date); valueList.Add(value); metaList.Add(new ParameterNodePoint(date, value)); dictColumn.Add(long.Parse(rows[i][0].ToString()), value.ToString()); if (cancellation?.Token.IsCancellationRequested == true) return; } bigTable.Add(fullName.Replace(".", ""), dictColumn); System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => { DataGridTextColumn dataGridTextColumn2 = new DataGridTextColumn(); dataGridTextColumn2.Header = fullName; dataGridTextColumn2.Binding = new System.Windows.Data.Binding(fullName.Replace(".", "")); var MyStyle = new Style(typeof(DataGridCell)) { Setters = { new Setter(TextBlock.TextAlignmentProperty, TextAlignment.Center) } }; dataGridTextColumn2.CellStyle = MyStyle; dataGrid.Columns.Add(dataGridTextColumn2); })); if (cancellation?.Token.IsCancellationRequested == true) return; dataSeries.Append(dateList, valueList, metaList); } if (cancellation?.Token.IsCancellationRequested == true) return; // 每一轮更新完毕后会恢复Chart刷新, // 数据量太小时会频繁挂起和恢复Chart,导致Chart不刷新,需要稍等一下UI。 Thread.Sleep(50); } System.Windows.Application.Current.Dispatcher.BeginInvoke(new Action(() => { System.Data.DataTable dt = new System.Data.DataTable(); dt = new System.Data.DataTable($"Temp"); dt.Columns.Add("internaltimestamp", typeof(long)); foreach (var item in bigTable.Keys) { dt.Columns.Add(item, typeof(string)); } if (bigTable.Count > 0) { foreach (var subitem in bigTable[bigTable.FirstOrDefault().Key].Keys) { var row = dt.NewRow(); row[0] = subitem; foreach (var col in bigTable.Keys) { row[col] = bigTable[col][subitem]; } dt.Rows.Add(row); } } dataGrid.ItemsSource = dt.DefaultView; })); } public void Exporting(object sender, EventArgs e) { BusyIndicatorContent = "Exporting Start ..."; IsBusy = true; } public void Exported(object sender, EventArgs e) { IsBusy = false; } public void Deleted(object sender, EventArgs e) { ((DataView)View).tvParameterNodes.ClearPresetGroupSelectionOnly(); } public void ProgressUpdating(object sender, ProgressUpdatingEventArgs e) { _progCsvExport.Report(e); } #endregion } public class StringToDateTimeStringConvert : IValueConverter { public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { return new DateTime((Int64)value).ToString("yyyy/MM/dd HH:mm:ss"); } public object ConvertBack(object value, Type targetTypes, object parameter, System.Globalization.CultureInfo culture) { return null; } } }