/************************************************************************ *@file FrameworkLocal\UIClient\CenterViews\Core\UserControls\DataViewDataGrid.cs * @author Su Liang * @Date 2022-08-01 * * @copyright © Sicentury Inc. * * @brief Reconstructed to support rich functions. * * @details * *****************************************************************************/ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Forms; using System.Windows.Input; using MECF.Framework.UI.Client.CenterViews.Core.Charting; using MECF.Framework.UI.Client.CenterViews.Core.EventArgs; using SciChart.Charting.Visuals.RenderableSeries; using Sicentury.Core.EventArgs; using MessageBox = System.Windows.MessageBox; namespace MECF.Framework.UI.Client.CenterViews.Core.UserControls { /// /// Interaction logic for DataViewDataGrid.xaml /// public partial class DataViewDataGrid { #region Variables /// /// 正在导出数据事件。 /// public event EventHandler Exporting; /// /// 导出数据完毕事件。 /// 导出过程异常仍触发此事件。 /// public event EventHandler Exported; /// /// 正在删除曲线事件。 /// public event EventHandler Deleting; /// /// 删除曲线完成事件。 /// public event EventHandler Deleted; /// /// 常时操作的进度信息更新事件。 /// public event EventHandler ProgressMessageUpdating; /// /// 取消操作。 /// private CancellationTokenSource _cancellationTokenSource; /// /// 在UI线程上报告进度。 /// private readonly IProgress _progress; #endregion #region Constructors public DataViewDataGrid() { _progress = new Progress(e => { ProgressMessageUpdating?.Invoke(this, e); }); } private void ItemsSourceOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { switch (e.Action) { case NotifyCollectionChangedAction.Add: RegisterPropertiesChangedEvent(e.NewItems); break; case NotifyCollectionChangedAction.Replace: RegisterPropertiesChangedEvent(e.NewItems); UnRegisterPropertiesChangedEvent(e.OldItems); break; case NotifyCollectionChangedAction.Remove: UnRegisterPropertiesChangedEvent(e.OldItems); break; case NotifyCollectionChangedAction.Reset: UnRegisterPropertiesChangedEvent(e.OldItems); RegisterPropertiesChangedEvent(e.NewItems); break; case NotifyCollectionChangedAction.Move: default: // Ignore. break; } } private void RegisterPropertiesChangedEvent(IList collection) { foreach (var item in collection) { if (item is SicFastLineSeries series) { series.PropertyChanged += SeriesOnPropertyChanged; } } } private void UnRegisterPropertiesChangedEvent(IList collection) { foreach (var item in collection) { if (item is SicFastLineSeries series) { series.PropertyChanged -= SeriesOnPropertyChanged; } } } private void SeriesOnPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(IRenderableSeries.IsVisible)) { // 设置标题栏中CheckBox的IsChecked属性 var group = ItemsSource.ToList() .Cast() .GroupBy(x => x.IsVisible) .ToList(); if (group.Count() > 1) { IsVisibleInColumnHeader = null; } else if (group.Count() == 1) { var visible = group[0].Key; IsVisibleInColumnHeader = visible; } } } #endregion #region Properties public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( "ItemsSource", typeof(ChartingLineSeriesCollection), typeof(DataViewDataGrid), new PropertyMetadata(default(ChartingLineSeriesCollection), (sender, e) => { //if (!(sender is DataViewDataGrid dg)) // return; //if (e.NewValue is ChartingLineSeriesCollection newColl) //{ // newColl.CollectionChanged += dg.ItemsSourceOnCollectionChanged; // dg.RegisterPropertiesChangedEvent(newColl); //} //if (e.OldValue is ChartingLineSeriesCollection oldColl) //{ // oldColl.CollectionChanged -= dg.ItemsSourceOnCollectionChanged; // dg.UnRegisterPropertiesChangedEvent(oldColl); //} })); /// /// 设置或返回DataGrid数据源。 /// public ChartingLineSeriesCollection ItemsSource { get => (ChartingLineSeriesCollection)GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public static readonly DependencyProperty IsShowStatisticColumnProperty = DependencyProperty.Register( nameof(IsShowStatisticColumn), typeof(bool), typeof(DataViewDataGrid), new PropertyMetadata(default(bool))); /// /// 设置或返回是否显示统计数据列。 /// public bool IsShowStatisticColumn { get => (bool)GetValue(IsShowStatisticColumnProperty); set => SetValue(IsShowStatisticColumnProperty, value); } public static readonly DependencyProperty IsVisibleInColumnHeaderProperty = DependencyProperty.Register( "IsVisibleInColumnHeader", typeof(bool?), typeof(DataViewDataGrid), new PropertyMetadata(default(bool?), (sender, e) => { if (!(sender is DataViewDataGrid dg)) return; if (e.Property.Name != nameof(IsVisibleInColumnHeader)) return; if (!(e.NewValue is bool visible)) return; // 如果CheckBox被选中状态,则本次点击后隐藏所有序列。 foreach (var series in dg.ItemsSource) { series.IsVisible = visible; } })); public bool? IsVisibleInColumnHeader { get => (bool?)GetValue(IsVisibleInColumnHeaderProperty); set => SetValue(IsVisibleInColumnHeaderProperty, value); } #endregion #region Methods protected override void OnInitialized(System.EventArgs e) { InitializeComponent(); base.OnInitialized(e); } /// /// 取消操作。 /// public void CancelOperation() { if (_cancellationTokenSource?.Token.CanBeCanceled == true) _cancellationTokenSource.Cancel(); } private void OnProgressMessageUpdating(int currentProgress, int totalProgress, string message) { _progress.Report(new ProgressUpdatingEventArgs(currentProgress, totalProgress, message)); } #endregion #region Events /// /// 导出全部曲线数据。 /// /// /// private async void BtnExportAll_OnClick(object sender, RoutedEventArgs e) { if (!(ItemsSource is ChartingLineSeriesCollection collection)) return; try { if (collection.Count == 0) { MessageBox.Show($"Please select the data you want to export.", "Export", MessageBoxButton.OK, MessageBoxImage.Warning); return; } #if EXPORT_TO_CSV var dlg = new SaveFileDialog { DefaultExt = ".xlsx", // Default file extension Filter = "Excel数据表格文件(*.csv)|*.csv", // Filter files by extension FileName = $"{collection.DisplayName}_{DateTime.Now:yyyyMMdd_HHmmss}" }; #else var dlg = new SaveFileDialog { DefaultExt = ".xlsx", // Default file extension Filter = "Excel数据表格文件(*.xlsx)|*.xlsx", // Filter files by extension FileName = $"{DisplayName}_{DateTime.Now:yyyyMMdd_HHmmss}" }; #endif var ret = dlg.ShowDialog(); // Show open file dialog box if (ret == DialogResult.OK) // Process open file dialog box results { Exporting?.Invoke(this, System.EventArgs.Empty); _cancellationTokenSource = new CancellationTokenSource(); var sw = new Stopwatch(); sw.Restart(); #if EXPORT_TO_CSV var columns = new List(); #else var ds = new DataSet(); ds.Tables.Add(new DataTable(dlg.FileName)); ds.Tables[0].Columns.Add("Time"); ds.Tables[0].Columns[0].DataType = typeof(DateTime); #endif var timeValue = new Dictionary(); var dataSeriesCollection = collection.Cast().Select(x => x.GetDataSeries()).ToList(); await Task.Run(() => { for (var i = 0; i < dataSeriesCollection.Count; i++) { OnProgressMessageUpdating( 50, 100, $"Exporting {dataSeriesCollection[i].SeriesName} {i}/{dataSeriesCollection.Count} ..."); var points = dataSeriesCollection[i].Metadata .Cast().ToList(); for (var n = 0; n < points.Count; n++) { var p = points[n]; if (!timeValue.ContainsKey(p.Time)) timeValue[p.Time] = new double[collection.Count]; timeValue[p.Time][i] = p.Value; if (_cancellationTokenSource.Token.IsCancellationRequested) break; } #if EXPORT_TO_CSV columns.Add((collection[i] as SicFastLineSeries)?.DataName ?? "Unknown Col"); #else ds.Tables[0].Columns.Add((SelectedData[i] as SicFastLineSeries)?.DataName); ds.Tables[0].Columns[i + 1].DataType = typeof(double); #endif if (_cancellationTokenSource.Token.IsCancellationRequested) break; } }, _cancellationTokenSource.Token).ContinueWith(t => { if (t.IsCanceled || t.IsFaulted) return; #if EXPORT_TO_CSV var csvBuilder = new StringBuilder(); csvBuilder.Append("Time,"); csvBuilder.AppendLine(string.Join(",", columns)); var totalPoints = (double)timeValue.Count; var processedPoints = 0d; var lastPercent = 0d; foreach (var tv in timeValue) { csvBuilder.Append($" {tv.Key:yyyy/MM/dd HH:mm:ss.fff}"); csvBuilder.Append(","); csvBuilder.AppendLine(string.Join(",", tv.Value)); if (_cancellationTokenSource?.Token.IsCancellationRequested == true) return; processedPoints++; var currentPercent = (int)(processedPoints / totalPoints * 100); if (currentPercent > lastPercent) { /* * 不要太频繁更新进度,否则会导致UI卡顿,每增长1%更新一次进度。 * currentPercent和lastPercent必须是整数,以将进度更新的频率控制在要求内。 */ OnProgressMessageUpdating( 60, 100, $"Building report content ({currentPercent}%) ..."); lastPercent = currentPercent; } } OnProgressMessageUpdating( 90, 100, $"Writing to file ..."); using (_cancellationTokenSource.Token.Register(Thread.CurrentThread.Abort)) { File.WriteAllText(dlg.FileName, csvBuilder.ToString()); } #else OnProgressMessageUpdating( 60, 100, $"Building report content ..."); foreach (var item in timeValue) { var row = ds.Tables[0].NewRow(); row[0] = item.Key; for (var j = 0; j < item.Value.Length; j++) { row[j + 1] = item.Value[j]; } ds.Tables[0].Rows.Add(row); } OnProgressMessageUpdating( 90, 100, $"Writing to file ..."); using (_cancellationTokenSource.Token.Register(Thread.CurrentThread.Abort)) { if (!ExcelHelper.ExportToExcel(dlg.FileName, ds, out var reason)) { MessageBox.Show($"Export failed, {reason}", "Export", MessageBoxButton.OK, MessageBoxImage.Warning); return; } } #endif }); sw.Stop(); Debug.WriteLine($"Export costs {sw.ElapsedMilliseconds}ms"); Debug.WriteLine($"Total Lines {timeValue.Count}"); if (_cancellationTokenSource?.Token.IsCancellationRequested == true) return; Exported?.Invoke(this, System.EventArgs.Empty); MessageBox.Show($"Exporting succeed, file save as {dlg.FileName}", "Export", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (ThreadAbortException) { // 操作取消。 } catch (AggregateException ae) { var ex = ae.Flatten().InnerExceptions; throw ex.FirstOrDefault() ?? ae; } catch (Exception ex) { MessageBox.Show($"Unable to export data, {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } finally { Exported?.Invoke(this, System.EventArgs.Empty); } } /// /// 导出单条曲线数据。 /// /// /// private async void BtnExport_OnPreviewMouseUp(object sender, MouseButtonEventArgs e) { if (!(ItemsSource is ChartingLineSeriesCollection collection)) return; if (!(sender is TextBlock btn)) return; if (!(btn.DataContext is SicFastLineSeries series)) return; try { #if EXPORT_TO_CSV var dlg = new SaveFileDialog { DefaultExt = ".xlsx", // Default file extension Filter = "Excel数据表格文件(*.csv)|*.csv", // Filter files by extension FileName = $"{series.DisplayName}_{DateTime.Now:yyyyMMdd_HHmmss}" }; #else var dlg = new SaveFileDialog { DefaultExt = ".xlsx", // Default file extension Filter = "Excel数据表格文件(*.xlsx)|*.xlsx", // Filter files by extension FileName = $"{DisplayName}_{DateTime.Now:yyyyMMdd_HHmmss}" }; #endif var result = dlg.ShowDialog(); // Show open file dialog box if (result == DialogResult.OK) // Process open file dialog box results { _cancellationTokenSource = new CancellationTokenSource(); Exporting?.Invoke(this, System.EventArgs.Empty); var sw = new Stopwatch(); sw.Restart(); #if EXPORT_TO_CSV var columns = new List(); #else var ds = new DataSet(); ds.Tables.Add(new DataTable(cp.DataName)); ds.Tables[0].Columns.Add("Time"); ds.Tables[0].Columns[0].DataType = typeof(DateTime); ds.Tables[0].Columns.Add(cp.DataName); ds.Tables[0].Columns[1].DataType = typeof(double); #endif var ds = series?.GetDataSeries(); var points = ds?.Metadata.Cast().ToList(); if (points == null) { MessageBox.Show($"Unable to find meta points from the series {series.DataName}.", "Error", MessageBoxButton.OK, MessageBoxImage.Error); return; } var csvBuilder = new StringBuilder(); await Task.Run(() => { using (_cancellationTokenSource.Token.Register(Thread.CurrentThread.Abort)) { #if EXPORT_TO_CSV csvBuilder.AppendLine($"Time,{ds.SeriesName}"); // table header #endif OnProgressMessageUpdating( 50, 100, $"Exporting data ..."); for (var i = 0; i < points.Count; i++) { var p = points[i]; #if EXPORT_TO_CSV csvBuilder.AppendLine($" {p.Time:yyyy/MM/dd HH:mm:ss.fff},{p.Value}"); #else var row = ds.Tables[0].NewRow(); row[0] = p.Time; row[1] = p.Value; ds.Tables[0].Rows.Add(row); #endif } OnProgressMessageUpdating( 90, 100, $"Writing to file ..."); #if EXPORT_TO_CSV File.WriteAllText(dlg.FileName, csvBuilder.ToString()); #else if (!ExcelHelper.ExportToExcel(dlg.FileName, ds, out var reason)) { MessageBox.Show($"Export failed, {reason}", "Export", MessageBoxButton.OK, MessageBoxImage.Warning); return; } #endif } }); sw.Stop(); Debug.WriteLine($"Export costs {sw.ElapsedMilliseconds}ms"); Debug.WriteLine($"Total Lines {points?.Count}"); Exported?.Invoke(this, System.EventArgs.Empty); MessageBox.Show($"Export succeed, file save as {dlg.FileName}", "Export", MessageBoxButton.OK, MessageBoxImage.Information); } } catch (ThreadAbortException) { // 操作取消。 } catch (Exception ex) { MessageBox.Show($"Unable to export data, {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } finally { Exported?.Invoke(this, System.EventArgs.Empty); } } /// /// 删除全部曲线。 /// /// /// private void BtnDeleteAll_OnClick(object sender, RoutedEventArgs e) { if (!(ItemsSource is ChartingLineSeriesCollection collection)) return; try { var list = collection.Cast().ToList(); var args = new RenderableSeriesDeletingEventArgs(list); Deleting?.Invoke(this, args); if (args.Cancel) return; var total = collection.Count; for (var i = total - 1; i >= 0; i--) { list[i].BackendParameterNode.IsSelected = false; } collection.Clear(); Deleted?.Invoke(this, args); } catch (Exception ex) { MessageBox.Show($"It's failed to delete all series, {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// 删除单条曲线。 /// /// /// private void BtnDelete_OnPreviewMouseUp(object sender, MouseButtonEventArgs e) { if (!(ItemsSource is ChartingLineSeriesCollection collection)) return; if (!(sender is TextBlock btn)) return; if (!(btn.DataContext is SicFastLineSeries series)) return; try { var args = new RenderableSeriesDeletingEventArgs( new List(new[] { series })); Deleting?.Invoke(this, args); if (args.Cancel) return; series.BackendParameterNode.IsSelected = false; collection.Remove(series); Deleted?.Invoke(this, args); } catch (Exception ex) { MessageBox.Show($"It's failed to delete series, {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error); } } /// /// 更换曲线颜色。 /// /// /// private void BtnChangeSeriesColor_OnPreviewMouseUp(object sender, MouseButtonEventArgs e) { var dlg = new System.Windows.Forms.ColorDialog(); if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; if (!(sender is Border bdr)) return; if (bdr.DataContext is SicFastLineSeries series) series.Stroke = new System.Windows.Media.Color() { A = dlg.Color.A, B = dlg.Color.B, G = dlg.Color.G, R = dlg.Color.R }; } #endregion } }