using InABox.Core; using InABox.Wpf; using InABox.WPF; using Syncfusion.Data; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Imaging; using SharpVectors.Converters; using SharpVectors.Renderers.Wpf; using Color = System.Drawing.Color; namespace InABox.DynamicGrid; public abstract class BaseDynamicGrid : ContentControl, IDynamicGridUIComponentParent { public static readonly DependencyProperty UseWaitCursorProperty = DependencyProperty.Register(nameof(UseWaitCursor), typeof(bool), typeof(BaseDynamicGrid)); public bool UseWaitCursor { get => (bool)GetValue(UseWaitCursorProperty); set => SetValue(UseWaitCursorProperty, value); } public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register( nameof(ItemsSource), typeof(object), typeof(BaseDynamicGrid), new PropertyMetadata(null, DoItemsSourceChanged) ); private static void DoItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is BaseDynamicGrid dynamicGrid) { dynamicGrid.OnItemSourceChanged(e.NewValue); } } public object? ItemsSource { get => GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public abstract void OnItemSourceChanged(object value); protected enum ClipAction { Cut, Copy } private IDynamicGridUIComponent UIComponent; private UIElement? _header; private readonly Button Add; public bool bRefreshing; bool IDynamicGridUIComponentParent.IsRefreshing => bRefreshing; private readonly Label ClipboardSpacer; private readonly Button Copy; private readonly Label Count; private readonly Border Disabler; private readonly DynamicActionColumn? drag; private readonly Button Delete; private readonly DockPanel Docker; private readonly Button Edit; private readonly Label EditSpacer; private readonly Button? ExportButton; private readonly Label ExportSpacer; private readonly Button? DuplicateBtn; private readonly Button SwitchViewBtn; private readonly Button? Help; private readonly Button? ImportButton; private readonly Grid Layout; private readonly Label Loading; private readonly DoubleAnimation LoadingFader = new(1d, 0.2d, new Duration(TimeSpan.FromSeconds(2))) { AutoReverse = true }; private readonly Button Print; private readonly Label PrintSpacer; private readonly StackPanel LeftButtonStack; private readonly StackPanel RightButtonStack; protected DynamicGridRowStyleSelector RowStyleSelector; protected virtual bool CanDuplicate { get; } = false; #region Events private event IDynamicGrid.ReconfigureEvent? _onReconfigure; public event IDynamicGrid.ReconfigureEvent? OnReconfigure { add { _onReconfigure += value; Reconfigure(); } remove { _onReconfigure -= value; Reconfigure(); } } public OnGetDynamicGridRowStyle? OnGetRowStyle { get; set; } public event OnPrintData? OnPrintData; public event BeforeRefreshEventHandler? BeforeRefresh; public event AfterRefreshEventHandler? AfterRefresh; /// /// Called when an item is selected in the grid. It is not called if is not . /// /// /// It is unnecessary to use this if within a grid. Instead, override . /// public event SelectItemHandler? OnSelectItem; public event OnCellDoubleClick? OnCellDoubleClick; public event EventHandler? OnChanged; public delegate void BeforeSelectionEvent(CancelEventArgs cancel); public event BeforeSelectionEvent? OnBeforeSelection; protected virtual void Changed() { } public virtual void DoChanged() { Changed(); OnChanged?.Invoke(this, EventArgs.Empty); } public event OnFilterRecord? OnFilterRecord; public event OnDoubleClick? OnDoubleClick; #endregion protected DynamicGridSettings Settings { get; set; } public BaseDynamicGrid() : base() { UseWaitCursor = true; Options = new DynamicGridOptions(); Options.OnChanged += () => { _hasLoadedOptions = true; OptionsChanged(); }; ActionColumns = new DynamicActionColumns(); ColumnGroupings = new DynamicGridColumnGroupings(); RowStyleSelector = GetRowStyleSelector(); RowStyleSelector.GetStyle += (row, style) => GetRowStyle(row, style); IsReady = false; Data = new CoreTable(); drag = new DynamicImageColumn(InABox.Wpf.Resources.drag.AsBitmapImage()) { Position = DynamicActionColumnPosition.Start }; VisibleColumns = new DynamicGridColumns(); PreInit(); UIComponent = CreateUIComponent(); Loading = new Label(); Loading.Content = "Loading..."; Loading.Foreground = new SolidColorBrush(Colors.White); Loading.VerticalContentAlignment = VerticalAlignment.Center; Loading.HorizontalContentAlignment = HorizontalAlignment.Center; Loading.Visibility = Visibility.Collapsed; Loading.SetValue(Panel.ZIndexProperty, 999); Loading.SetValue(Grid.RowProperty, 1); Loading.FontSize = 14.0F; LoadingFader.Completed += (sender, args) => { if (Loading.Visibility == Visibility.Visible) { //Logger.Send(LogType.Information, this.GetType().EntityName().Split(".").Last(), "Loading Fader Restarting"); Loading.BeginAnimation(Label.OpacityProperty, LoadingFader); } }; if(this is IHelpDynamicGrid helpGrid) { Help = CreateButton(Wpf.Resources.help.AsBitmapImage(Color.White)); Help.Margin = new Thickness(0, 2, 2, 0); Help.SetValue(DockPanel.DockProperty, Dock.Right); Help.Click += (o, e) => ShowHelp(helpGrid.HelpSlug()); } Add = CreateButton(Wpf.Resources.add.AsBitmapImage(Color.White)); Add.Margin = new Thickness(0, 2, 2, 0); Add.Click += Add_Click; Edit = CreateButton(Wpf.Resources.pencil.AsBitmapImage(Color.White)); Edit.Margin = new Thickness(0, 2, 2, 0); Edit.Click += Edit_Click; SwitchViewBtn = CreateButton(Wpf.Resources.alter.AsBitmapImage()); SwitchViewBtn.Margin = new Thickness(0, 2, 2, 0); SwitchViewBtn.Click += SwitchView_Click; EditSpacer = new Label { Width = 5 }; Print = CreateButton(Wpf.Resources.print.AsBitmapImage(Color.White)); Print.Margin = new Thickness(0, 2, 2, 0); Print.Click += (o, e) => DoPrint(o); PrintSpacer = new Label { Width = 5 }; Copy = CreateButton(Wpf.Resources.duplicate.AsBitmapImage(Color.White), tooltip: "Duplicate Rows"); Copy.Margin = new Thickness(0, 2, 2, 0); Copy.Click += Copy_Click; ClipboardSpacer = new Label { Width = 5 }; if(this is IExportDynamicGrid) { ExportButton = CreateButton(ImageUtils.SvgToBitmapImage(Wpf.Resources.download)); ExportButton.ToolTip = "Export to File"; ExportButton.Margin = new Thickness(0, 2, 2, 0); ExportButton.Click += ExportButtonClick; } if(this is IImportDynamicGrid) { ImportButton = CreateButton(ImageUtils.SvgToBitmapImage(Wpf.Resources.upload)); ImportButton.ToolTip = "Import from File"; ImportButton.Margin = new Thickness(0, 2, 2, 0); ImportButton.Click += ImportButton_Click; } ExportSpacer = new Label { Width = 5 }; LeftButtonStack = new StackPanel(); LeftButtonStack.Orientation = Orientation.Horizontal; LeftButtonStack.SetValue(DockPanel.DockProperty, Dock.Left); if(Help is not null) { LeftButtonStack.Children.Add(Help); } LeftButtonStack.Children.Add(Add); LeftButtonStack.Children.Add(Edit); LeftButtonStack.Children.Add(SwitchViewBtn); //Stack.Children.Add(MultiEdit); LeftButtonStack.Children.Add(EditSpacer); LeftButtonStack.Children.Add(Print); LeftButtonStack.Children.Add(PrintSpacer); LeftButtonStack.Children.Add(Copy); LeftButtonStack.Children.Add(ClipboardSpacer); if(ExportButton is not null) { LeftButtonStack.Children.Add(ExportButton); } if(ImportButton is not null) { LeftButtonStack.Children.Add(ImportButton); } if(ExportButton is not null || ImportButton is not null) { LeftButtonStack.Children.Add(ExportSpacer); } RightButtonStack = new StackPanel(); RightButtonStack.Orientation = Orientation.Horizontal; RightButtonStack.SetValue(DockPanel.DockProperty, Dock.Right); Delete = CreateButton(Wpf.Resources.delete.AsBitmapImage(Color.White)); Delete.Margin = new Thickness(2, 2, 0, 0); Delete.SetValue(DockPanel.DockProperty, Dock.Right); Delete.Click += Delete_Click; if(this is IDuplicateDynamicGrid) { DuplicateBtn = AddButton("Duplicate", Wpf.Resources.paste.AsBitmapImage(Color.White), DuplicateButton_Click); } Count = new Label(); Count.Height = 30; Count.Margin = new Thickness(0, 2, 0, 0); Count.VerticalContentAlignment = VerticalAlignment.Center; Count.HorizontalContentAlignment = HorizontalAlignment.Center; Count.SetValue(DockPanel.DockProperty, Dock.Left); Docker = new DockPanel(); Docker.SetValue(Grid.RowProperty, 2); Docker.SetValue(Grid.ColumnProperty, 0); Docker.Children.Add(LeftButtonStack); Docker.Children.Add(Delete); Docker.Children.Add(RightButtonStack); Docker.Children.Add(Count); Layout = new Grid(); Layout.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) }); Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) }); Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) }); Layout.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Auto) }); var control = UIComponent.Control; control.SetValue(Grid.RowProperty, 1); Layout.Children.Add(control); Layout.Children.Add(Loading); Layout.Children.Add(Docker); Disabler = new Border() { BorderBrush = new SolidColorBrush(Colors.Transparent), Background = new SolidColorBrush(Colors.DimGray) { Opacity = 0.2 }, Visibility = Visibility.Collapsed, }; Disabler.SetValue(Canvas.ZIndexProperty, 99); Disabler.SetValue(Grid.RowSpanProperty, 3); Layout.Children.Add(Disabler); //Scroll.ApplyTemplate(); Content = Layout; IsEnabledChanged += (sender, args) => { Disabler.Visibility = Equals(args.NewValue, true) ? Visibility.Collapsed : Visibility.Visible; }; Settings = LoadSettings(); Init(); Reconfigure(); } protected virtual void PreInit() { } public CoreRow GetVisibleRow(int index) => UIComponent.GetVisibleRow(index); #region IDynamicGridUIComponentParent protected virtual IDynamicGridUIComponent CreateUIComponent() { return new DynamicGridGridUIComponent() { Parent = this }; } protected IDynamicGridUIComponent GetUIComponent() => UIComponent; bool IDynamicGridUIComponentParent.CanFilter() { return !Options.ReorderRows || !Options.EditRows; } bool IDynamicGridUIComponentParent.CanSort() { return !Options.ReorderRows || !Options.EditRows; } DynamicGridRowStyleSelector IDynamicGridUIComponentParent.RowStyleSelector => RowStyleSelector; void IDynamicGridUIComponentParent.BeforeSelection(CancelEventArgs cancel) { BeforeSelection(cancel); } void IDynamicGridUIComponentParent.SelectItems(CoreRow[] rows) { SelectItems(rows); } void IDynamicGridUIComponentParent.HandleKey(KeyEventArgs e) { if (Options.ReorderRows) { if (e.Key == Key.X && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { CutToClipBuffer(); } else if (e.Key == Key.C && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { CopyToClipBuffer(); } else if (e.Key == Key.V && (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control) { PasteFromClipBuffer(); } else if (e.Key == Key.Escape) { ResetClipBuffer(); InvalidateGrid(); } } } void IDynamicGridUIComponentParent.DoubleClickCell(CoreRow? row, DynamicColumnBase? column) { var args = new DynamicGridCellClickEventArgs(row, column); if (OnCellDoubleClick is not null) { OnCellDoubleClick?.Invoke(this, args); if (args.Handled) return; } if (row is not null) DoDoubleClick(this, args); } void IDynamicGridUIComponentParent.ExecuteActionColumn(DynamicActionColumn column, CoreRow[]? rows) { var bRefresh = false; if(rows is null) { bRefresh = column.Action?.Invoke(null) ?? false; } else { foreach (var row in rows) if (column.Action?.Invoke(row) == true) bRefresh = true; } if (bRefresh) Dispatcher.BeginInvoke(() => { Refresh(true, true); }); } void IDynamicGridUIComponentParent.OpenColumnMenu(DynamicColumnBase column) { if(column is DynamicMenuColumn menuColumn) { menuColumn.Action?.Invoke(SelectedRows.FirstOrDefault()); } else if(column is DynamicActionColumn actionColumn) { var menu = actionColumn?.ContextMenu?.Invoke(SelectedRows); if (menu != null && menu.Items.Count > 0) { menu.IsOpen = true; } } } void IDynamicGridUIComponentParent.UpdateRecordCount(int count) { Count.Content = FormatRecordCount(count); } protected virtual string FormatRecordCount(int count) => $"{count} Records"; void IDynamicGridUIComponentParent.LoadColumnsMenu(ContextMenu menu) { menu.AddItem("Select Columns", null, SelectColumnsClick); LoadColumnsMenu(menu); } void IDynamicGridUIComponentParent.DragOver(object sender, DragEventArgs e) { HandleDragOver(sender, e); } void IDynamicGridUIComponentParent.Drop(object sender, DragEventArgs e) { if (!Options.DragTarget) return; if(DynamicGridUtils.TryGetDropData(e, out var entityType, out var table)) { OnDragEnd(entityType, table, e); } else { HandleDragDrop(sender, e); } } void IDynamicGridUIComponentParent.DragStart(object? sender, CoreRow[] rows) { Logger.Send(LogType.Information, "", "RowDragDropController_DragStart"); if (!Options.DragSource) return; OnRowsDragStart(rows); } public void UIFilterChanged(object sender) => DoFilterChanged(); //void IDynamicGridUIComponentParent.UIFilterChanged(object sender) => DoFilterChanged(); protected virtual void DoFilterChanged() { } private Dictionary ColumnFilters { get; set; } = new(); IDynamicGridColumnFilter? IBaseDynamicGrid.GetColumnFilter(DynamicColumnBase column) => GetColumnFilter(column); protected IDynamicGridColumnFilter? GetColumnFilter(DynamicColumnBase column) { if(!ColumnFilters.TryGetValue(column, out var filter)) { filter = GenerateColumnFilter(column); ColumnFilters.Add(column, filter); } return filter; } protected virtual IDynamicGridColumnFilter? GenerateColumnFilter(DynamicColumnBase column) { if(column is DynamicGridColumn gc) { if(gc.Editor is DateTimeEditor || gc.Editor is DateEditor) { return new DateTreeDynamicGridColumnFilter(this, column); } else { return new StandardDynamicGridColumnFilter(this, column); } } else if(column is DynamicActionColumn ac) { if(ac.GetFilter is not null) { return ac.GetFilter(); } else if(ac is DynamicTextColumn textColumn) { return new StandardDynamicGridColumnFilter(this, textColumn); } else { return null; } } else { return null; } } #endregion protected virtual DynamicGridRowStyleSelector GetRowStyleSelector() { return new SimpleDynamicGridRowStyleSelector(); } protected virtual DynamicGridStyle GetRowStyle(CoreRow row, DynamicGridStyle style) { DynamicGridStyle? result = null; if (ClipBuffer != null) if (ClipBuffer.Item2.Contains(row)) { var bgbrush = style.Background as SolidColorBrush; var bgcolor = bgbrush != null ? bgbrush.Color : Colors.Transparent; result = new DynamicGridRowStyle(style); result.Background = ClipBuffer.Item1 == ClipAction.Cut ? new SolidColorBrush(bgcolor.MixColors(0.5, Colors.Orchid)) : new SolidColorBrush(bgcolor.MixColors(0.5, Colors.LightGreen)); result.Foreground = new SolidColorBrush(Colors.Gray); result.FontStyle = FontStyles.Italic; } result ??= OnGetRowStyle != null ? OnGetRowStyle(row, style) : style; return result; } protected virtual void BeforeSelection(CancelEventArgs cancel) { OnBeforeSelection?.Invoke(cancel); } public bool IsReady { get; protected set; } public UIElement? Header { get => _header; set { if (_header is not null && Layout.Children.Contains(_header)) Layout.Children.Remove(_header); _header = value; if (_header is not null) { _header.SetValue(Grid.RowProperty, 0); _header.SetValue(Grid.ColumnProperty, 0); _header.SetValue(Grid.ColumnSpanProperty, 2); Layout.Children.Add(_header); } } } /// /// Represents the unfiltered data in the grid. This is until is called. /// /// /// This differs from in that has been filtered by , /// whereas contains every record loaded from the database. /// public CoreTable? MasterData { get; set; } public DynamicGridColumns VisibleColumns { get; protected set; } public DynamicActionColumns ActionColumns { get; protected set; } private List ColumnList = new(); IList IBaseDynamicGrid.ColumnList => ColumnList; public CoreTable Data { get; set; } public double RowHeight { get => UIComponent.RowHeight; set => UIComponent.RowHeight = value; } public double HeaderHeight { get => UIComponent.HeaderRowHeight; set => UIComponent.HeaderRowHeight = value; } #region Options /// /// Initialise things like custom buttons; called once during construction. /// protected abstract void Init(); protected abstract void DoReconfigure(DynamicGridOptions options); private bool _hasLoadedOptions = false; protected virtual void OptionsChanged() { var reloadColumns = false; if(Help is not null) { Help.Visibility = Options.ShowHelp ? Visibility.Visible : Visibility.Collapsed; } Add.Visibility = Options.AddRows ? Visibility.Visible : Visibility.Collapsed; Edit.Visibility = Options.EditRows ? Visibility.Visible : Visibility.Collapsed; EditSpacer.Visibility = Options.AddRows || Options.EditRows ? Visibility.Visible : Visibility.Collapsed; Print.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed; PrintSpacer.Visibility = Options.Print ? Visibility.Visible : Visibility.Collapsed; Copy.Visibility = Options.ReorderRows ? Visibility.Visible : Visibility.Collapsed; ClipboardSpacer.Visibility = Options.ReorderRows ? Visibility.Visible : Visibility.Collapsed; if(ExportButton is not null) { ExportButton.Visibility = Options.ExportData ? Visibility.Visible : Visibility.Collapsed; } if(ImportButton is not null) { ImportButton.Visibility = Options.ImportData ? Visibility.Visible : Visibility.Collapsed; } ExportSpacer.Visibility = Options.ExportData || Options.ImportData ? Visibility.Visible : Visibility.Collapsed; SwitchViewBtn.Visibility = Options.DirectEdit ? Options.HideDirectEditButton ? Visibility.Collapsed : Visibility.Visible : Visibility.Collapsed; Count.Visibility = Options.RecordCount ? Visibility.Visible : Visibility.Collapsed; Delete.Visibility = Options.DeleteRows ? Visibility.Visible : Visibility.Collapsed; if (drag is not null) { var hasSequence = drag.Position == DynamicActionColumnPosition.Start; if (Options.ReorderRows) { if (!ActionColumns.Contains(drag)) { ActionColumns.Insert(0, drag); } } else { ActionColumns.Remove(drag); } if(hasSequence != Options.ReorderRows) { drag.Position = Options.ReorderRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden; reloadColumns = true; } } if (DuplicateBtn != null) DuplicateBtn.Visibility = Visibility.Collapsed; if (UIComponent.OptionsChanged()) { reloadColumns = true; } if(reloadColumns && IsReady) { Refresh(true, false); } } public bool IsDirectEditMode() { return Options.DirectEdit && (Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.DirectEdit || Settings.ViewMode == DynamicGridSettings.DynamicGridViewMode.Default); } private void SwitchView_Click(object sender, RoutedEventArgs e) { Settings.ViewMode = Settings.ViewMode switch { DynamicGridSettings.DynamicGridViewMode.Default => DynamicGridSettings.DynamicGridViewMode.Normal, DynamicGridSettings.DynamicGridViewMode.Normal => DynamicGridSettings.DynamicGridViewMode.DirectEdit, DynamicGridSettings.DynamicGridViewMode.DirectEdit or _ => DynamicGridSettings.DynamicGridViewMode.Normal }; SaveSettings(Settings); Reconfigure(); } public DynamicGridOptions Options { get; } protected void OnReconfigureEvent(DynamicGridOptions options) { _onReconfigure?.Invoke(options); } /// /// Configure custom buttons and options. /// public void Reconfigure(DynamicGridOptions options) { options.BeginUpdate().Clear(); DoReconfigure(options); OnReconfigureEvent(options); options.EndUpdate(); if (!_hasLoadedOptions) { _hasLoadedOptions = true; OptionsChanged(); } } public void Reconfigure() { Reconfigure(Options); } public void Reconfigure(IDynamicGrid.ReconfigureEvent onReconfigure) { OnReconfigure += onReconfigure; Reconfigure(); } #endregion protected virtual DynamicGridSettings LoadSettings() { return new DynamicGridSettings(); } protected virtual void SaveSettings(DynamicGridSettings settings) { } protected virtual void LoadColumnsMenu(ContextMenu menu) { } protected void UpdateCell(int row, string colname, object? value) { var coreRow = Data.Rows[row]; coreRow[colname] = value; UIComponent.UpdateCell(coreRow, colname, value); } protected void UpdateCell(CoreRow row, DynamicColumnBase column) { UIComponent.UpdateCell(row, column); } #region Row Selections protected CoreRow[] GetVisibleRows() { return UIComponent.GetVisibleRows(); } public CoreRow[] SelectedRows { get => UIComponent.SelectedRows; set => UIComponent.SelectedRows = value; } /// /// Call the event, and do any updating which needs to occur when items are selected. /// /// protected virtual void SelectItems(CoreRow[]? rows) { if (IsReady) OnSelectItem?.Invoke(this, new DynamicGridSelectionEventArgs(rows)); if(DuplicateBtn is not null) { DuplicateBtn.Visibility = CanDuplicate && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed; } } protected virtual void DoDoubleClick(object sender, DynamicGridCellClickEventArgs args) { if (IsDirectEditMode()) return; //SelectItems(SelectedRows); var e = new HandledEventArgs(false); OnDoubleClick?.Invoke(sender, e); if (e.Handled) return; if (Options.EditRows) DoEdit(); } #endregion #region Column Handling #region Column Grouping public DynamicGridColumnGroupings ColumnGroupings { get; set; } /// /// Create a new column header group, and return it for editing. /// /// public DynamicGridColumnGrouping AddColumnGrouping() { var group = new DynamicGridColumnGrouping(); ColumnGroupings.Add(group); return group; } /// /// Gets the current column header group, and if there is none, create a new one. /// /// public DynamicGridColumnGrouping GetColumnGrouping() { if(ColumnGroupings.Count == 0) { return AddColumnGrouping(); } return ColumnGroupings[^1]; } #endregion protected virtual DynamicGridColumns LoadColumns() { return GenerateColumns(); } /// /// Provide a set of columns which is the default for this grid. /// public abstract DynamicGridColumns GenerateColumns(); protected abstract void SaveColumns(DynamicGridColumns columns); public int DesiredWidth() { return UIComponent.DesiredWidth(); } /// /// Handle to configure column groups. /// /// /// This is called after , so by the time this is called, both /// and will be loaded, which means one can reference these in the column groups. ///
/// Note: is cleared before this function is called. ///
protected virtual void ConfigureColumnGroups() { } protected virtual void ConfigureColumns(DynamicGridColumns columns) { } public class ColumnsLoadedEventArgs : EventArgs { public List Columns { get; private set; } public DynamicGridColumnGroupings ColumnGroupings { get; private set; } public IEnumerable ActionColumns => Columns.OfType(); public IEnumerable DataColumns => Columns.OfType(); public ColumnsLoadedEventArgs(List columns, DynamicGridColumnGroupings columnGroupings) { Columns = columns; ColumnGroupings = columnGroupings; } public DynamicGridColumn Add( Expression> member, int? width = null, string? caption = null, string? format = null, Alignment? alignment = null) { var col = DynamicGridColumns.CreateColumn(member, width: width, caption: caption, format: format, alignment: alignment); Columns.Add(col); return col; } } public delegate void ColumnsLoadedEvent(BaseDynamicGrid sender, ColumnsLoadedEventArgs args); public event ColumnsLoadedEvent? ColumnsLoaded; protected virtual void OnColumnsLoaded(List columns, DynamicGridColumnGroupings groupings) { ColumnsLoaded?.Invoke(this, new ColumnsLoadedEventArgs(columns, groupings)); } private void ReloadColumns() { ColumnFilters.Clear(); VisibleColumns = LoadColumns(); ConfigureColumns(VisibleColumns); ColumnGroupings.Clear(); ConfigureColumnGroups(); ColumnList = new List(); ColumnList.AddRange(ActionColumns.Where(x => x.Position == DynamicActionColumnPosition.Start)); ColumnList.AddRange(VisibleColumns); ColumnList.AddRange(ActionColumns.Where(x => x.Position == DynamicActionColumnPosition.End)); OnColumnsLoaded(ColumnList, ColumnGroupings); VisibleColumns.Clear(); VisibleColumns.AddRange(ColumnList.OfType()); UIComponent.RefreshColumns(ColumnList, ColumnGroupings); } #endregion #region Refresh / Reload protected bool IsPaging { get; set; } = false; protected virtual bool FilterRecord(CoreRow row) { if (OnFilterRecord is not null) return OnFilterRecord(row); return true; } private class RowRange(int rowIdx, int size) { public int RowIdx { get; set; } = rowIdx; public int Size { get; set; } = size; } protected abstract void ReloadData(CancellationToken token, Action action); private CancellationTokenSource? RefreshCancellationToken; public virtual void Refresh(bool reloadcolumns, bool reloaddata) { if (bRefreshing) return; if (!DoBeforeRefresh()) return; UIComponent.BeforeRefresh(); using var cursor = UseWaitCursor ? new WaitCursor() : null; Loading.Visibility = Visibility.Visible; Loading.BeginAnimation(Label.OpacityProperty, LoadingFader); bRefreshing = true; if (reloadcolumns) { ReloadColumns(); } if (reloaddata) { RefreshCancellationToken?.Cancel(); var tokenSource = new CancellationTokenSource(); RefreshCancellationToken = tokenSource; var token = tokenSource.Token; ReloadData(token, (table, exception) => { if (token.IsCancellationRequested) return; // Don't bother even checking exceptions if task was cancelled. if (exception != null) { Dispatcher.Invoke(() => { MessageWindow.ShowError("Sorry! We couldn't load the data.", exception); }); } else if (table is not null) { if (table.Offset == 0 || MasterData is null) { MasterData = table; Dispatcher.Invoke(() => { try { ProcessData(null); } catch (Exception) { } DoAfterRefresh(); bRefreshing = false; IsReady = true; }); } else { int idx = MasterData.Rows.Count; MasterData.AddPage(table); Dispatcher.Invoke(() => { try { ProcessData(new(idx, table.Rows.Count)); } catch (Exception) { } }); } } }); } else { ProcessData(null); DoAfterRefresh(); bRefreshing = false; IsReady = true; } } public void Shutdown() { RefreshCancellationToken?.Cancel(); } protected void NotifyBeforeRefresh(BeforeRefreshEventArgs args) => BeforeRefresh?.Invoke(this, args); protected void NotifyAfterRefresh(AfterRefreshEventArgs args) => AfterRefresh?.Invoke(this, args); protected bool OnBeforeRefresh() { return true; } private bool DoBeforeRefresh() { var result = OnBeforeRefresh(); if (result) { var args = new BeforeRefreshEventArgs() { Cancel = false }; NotifyBeforeRefresh(args); result = args.Cancel == false; } return result; } protected virtual void OnAfterRefresh() { } protected void DoAfterRefresh() { OnAfterRefresh(); NotifyAfterRefresh(new AfterRefreshEventArgs()); } /// /// Process the data from according to . /// /// /// Set to if this is the first page of data to be loaded. This will thus update the grid accordingly, /// clearing all current rows, resetting columns, selection, etc. If the is provided, this will add to the grid the rows /// according to the range from . /// private void ProcessData(RowRange? range) { if(range is null) { Data.Columns.Clear(); Data.Setters.Clear(); if (MasterData != null) foreach (var column in MasterData.Columns) Data.Columns.Add(column); } LoadData(range); } protected readonly Dictionary _recordmap = new(); public void UpdateRow(CoreRow row, Expression> column, TType value, bool refresh = true) { row.Set(column, value); _recordmap[row].Set(column, value); if (refresh) InvalidateRow(row); } public void UpdateRow(CoreRow row, string column, TType value, bool refresh = true) { row.Set(column, value); _recordmap[row].Set(column, value); if (refresh) InvalidateRow(row); } void IDynamicGridUIComponentParent.UpdateData(CoreRow row, string changedColumn, Dictionary updates) { var result = new Dictionary(); foreach (var (col, value) in updates) { UpdateRow(row, col.ColumnName, value, refresh: false); } } public void AddRow(CoreRow row) { if (MasterData is null) return; var masterrow = MasterData.NewRow(); MasterData.FillRow(masterrow, row); Refresh(false, false); } public void DeleteRow(CoreRow row) { if (MasterData is null) return; var masterrow = _recordmap[row]; MasterData.Rows.Remove(masterrow); Refresh(false, false); } /// /// Filter all given rows into , given that they match and . /// If is given, also updates the map from to . /// protected IList FilterRows( IEnumerable from, CoreTable into, Dictionary? recordMap = null, Func? filter = null) { var newRows = new List(); foreach (var row in from) if (FilterRecord(row) && filter?.Invoke(row) != false) { var newrow = into.NewRow(); for (var i = 0; i < into.Columns.Count; i++) { var value = i < row.Values.Count ? row.Values[i] : null; if (into.Columns[i].DataType.IsNumeric()) value = into.Columns[i].DataType.IsDefault(value) ? null : value; newrow.Values.Add(value); } newRows.Add(newrow); into.Rows.Add(newrow); recordMap?.TryAdd(newrow, row); } return newRows; } private void LoadData(RowRange? range) { if (MasterData is null) return; if(range is null) { ResetClipBuffer(); Data.Rows.Clear(); _recordmap.Clear(); FilterRows(MasterData.Rows, Data, _recordmap); InvalidateGrid(); SelectedRows = Array.Empty(); } else { var _newRows = FilterRows(Enumerable.Range(range.RowIdx, range.Size).Select(i => MasterData.Rows[i]), Data, _recordmap); UIComponent.AddPage(_newRows); } } public void InvalidateRow(CoreRow row) { UIComponent.InvalidateRow(row); } protected void InvalidateGrid() { if (RowStyleSelector != null) RowStyleSelector.Data = Data; UIComponent.RefreshData(Data); Loading.BeginAnimation(Label.OpacityProperty, null); Loading.Visibility = Visibility.Collapsed; } public void AddVisualFilter(string column, string value, FilterType filtertype = FilterType.Contains) { UIComponent.AddVisualFilter(column, value, filtertype); } protected List>> GetFilterPredicates() { return UIComponent.GetFilterPredicates(); } public object? GetData(CoreRow row, DynamicColumnBase column) { if(column is DynamicActionColumn ac) { return ac.Data(row); } else if(column is DynamicGridColumn gc) { return row[gc.ColumnName]; } else { return null; } } #endregion #region Item Manipulation #region Abstract/Virtual Functions /// /// Create a new row. /// protected abstract void NewRow(); /// /// Edit or create a new row and edit it. This should update the rows, and if it creates a new row, /// it should be added to the grid. /// protected abstract bool EditRows(CoreRow[]? rows); public abstract void DeleteRows(params CoreRow[] rows); protected virtual bool CanDeleteRows(params CoreRow[] rows) { return true; } private bool DuplicateButton_Click(Button button, CoreRow[] rows) { return DoDuplicate(rows); } private bool DoDuplicate(CoreRow[] rows) { return this is IDuplicateDynamicGrid grid && grid.DoDuplicate(rows); } #endregion #region Load/Save/Delete protected virtual void DoDelete() { var rows = SelectedRows.ToArray(); if (rows.Any()) if (CanDeleteRows(rows)) if (MessageBox.Show("Are you sure you wish to delete the selected records?", "Confirm Delete", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { DeleteRows(rows); SelectedRows = Array.Empty(); Refresh(false, true); DoChanged(); SelectItems(null); } } private void Delete_Click(object sender, RoutedEventArgs e) { DoDelete(); } #endregion #region Edit protected virtual void DoEdit() { if (SelectedRows.Length == 0) return; if (AddEditClick(SelectedRows)) { SelectItems(SelectedRows); } } private void Edit_Click(object sender, RoutedEventArgs e) { DoEdit(); } protected virtual void DoAdd(bool openEditorOnDirectEdit = false) { if (IsDirectEditMode() && !openEditorOnDirectEdit) { NewRow(); } else if (AddEditClick(null)) { Refresh(false, true); } } private void Add_Click(object sender, RoutedEventArgs e) { if (CanCreateRows()) DoAdd(); } BaseEditor IDynamicGridUIComponentParent.CustomiseEditor(DynamicGridColumn column, BaseEditor editor) => CustomiseEditor(column, editor); protected virtual BaseEditor CustomiseEditor(DynamicGridColumn column, BaseEditor editor) { return editor.CloneEditor(); } protected virtual bool CanCreateRows() { return true; } private bool AddEditClick(CoreRow[]? rows) { if (!IsEnabled || bRefreshing) return false; if (rows == null || rows.Length == 0) { if (!CanCreateRows()) return false; return EditRows(null); } else { return EditRows(rows); } } #endregion protected virtual void DoPrint(object sender) { OnPrintData?.Invoke(sender); } protected virtual void ShowHelp(string slug) { Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/" + slug) { UseShellExecute = true }); } void IDynamicGridUIComponentParent.MoveRows(InABox.Core.CoreRow[] rows, int index) => MoveRows(rows, index); #region ClipBuffer private Tuple? ClipBuffer; protected void ResetClipBuffer() { ClipBuffer = null; } protected void SetClipBuffer(ClipAction action, CoreRow[] rows) { ClipBuffer = new Tuple(action, rows); } private void CutToClipBuffer() { SetClipBuffer(ClipAction.Cut, SelectedRows); InvalidateGrid(); } private void CopyToClipBuffer() { SetClipBuffer(ClipAction.Copy, SelectedRows); InvalidateGrid(); } private void PasteFromClipBuffer() { if (ClipBuffer == null) return; var row = SelectedRows.FirstOrDefault(); MoveRows(ClipBuffer.Item2, row is not null ? (int)row.Index + 1 : Data.Rows.Count, isCopy: ClipBuffer.Item1 == ClipAction.Copy); } /// /// Reorder the given rows, to place them at ; that is, the first row of will /// be at after this method executes. If is , the items will copied, rather /// than moved. /// /// /// To move the rows to the end, should be equal to the number of rows in . /// protected abstract void MoveRows(CoreRow[] rows, int index, bool isCopy = false); private void Copy_Click(object sender, RoutedEventArgs e) { var rows = SelectedRows; if (rows.Length == 0) return; MoveRows(rows, rows[^1].Index + 1, isCopy: true); } #endregion #region Import / Export private void DoImport() { if(this is IImportDynamicGrid grid) { grid.DoImport(); } } private void ImportButton_Click(object sender, RoutedEventArgs e) { DoImport(); } public void Import() => DoImport(); private void DoExport() { if(this is IExportDynamicGrid grid) { grid.DoExport(); } } private void ExportButtonClick(object sender, RoutedEventArgs e) { DoExport(); } #endregion public void ScrollIntoView(CoreRow row) { UIComponent.ScrollIntoView(row); } #endregion #region Custom Buttons private Button CreateButton(ImageSource? image = null, string? text = null, string? tooltip = null) { var button = new Button(); button.SetValue(BorderBrushProperty, new SolidColorBrush(Colors.Gray)); button.SetValue(BorderThicknessProperty, new Thickness(0.75)); button.Height = 30; button.MinWidth = 30; UpdateButton(button, image, text, tooltip); return button; } public void UpdateButton(Button button, ImageSource? image, string? text, string? tooltip = null) { var stackPnl = new StackPanel(); stackPnl.Orientation = Orientation.Horizontal; //stackPnl.Margin = new Thickness(2); if (image != null) { var img = new Image(); img.Source = image; img.Margin = new Thickness(2); img.ToolTip = tooltip; stackPnl.Children.Add(img); } if (!string.IsNullOrEmpty(text)) { button.MaxWidth = double.MaxValue; var lbl = new Label(); lbl.Content = text; lbl.VerticalAlignment = VerticalAlignment.Stretch; lbl.VerticalContentAlignment = VerticalAlignment.Center; lbl.Margin = new Thickness(2, 0, 5, 0); lbl.ToolTip = ToolTip; stackPnl.Children.Add(lbl); } else button.MaxWidth = 30; button.Content = stackPnl; button.ToolTip = tooltip; } private bool bFirstButtonAdded = true; private bool AnyButtonsVisible() { if (Add.Visibility != Visibility.Collapsed) return true; if (Edit.Visibility != Visibility.Collapsed) return true; /*if (MultiEdit.Visibility != Visibility.Collapsed) return true;*/ if (ExportButton is not null && ExportButton.Visibility != Visibility.Collapsed) return true; return false; } public Button AddButton(string? caption, ImageSource? image, string? tooltip, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left) { var button = CreateButton(image, caption, tooltip); button.Margin = position == DynamicGridButtonPosition.Right ? new Thickness(2, 2, 0, 0) : bFirstButtonAdded && AnyButtonsVisible() ? new Thickness(0, 2, 0, 0) : new Thickness(0, 2, 2, 0); button.Padding = !String.IsNullOrWhiteSpace(caption) ? new Thickness(5, 1, 5, 1) : new Thickness(1); button.Tag = action; button.Click += Button_Click; if (position == DynamicGridButtonPosition.Right) RightButtonStack.Children.Add(button); else LeftButtonStack.Children.Add(button); bFirstButtonAdded = false; return button; } public Button AddButton(string? caption, ImageSource? image, DynamicGridButtonClickEvent action, DynamicGridButtonPosition position = DynamicGridButtonPosition.Left) { var result = AddButton(caption, image, null, action, position); return result; } private void Button_Click(object sender, RoutedEventArgs e) { var button = (Button)sender; var action = (DynamicGridButtonClickEvent)button.Tag; //CoreRow row = (CurrentRow > -1) && (CurrentRow < Data.Rows.Count) ? Data.Rows[this.CurrentRow] : null; if (action.Invoke(button, SelectedRows)) Refresh(false, true); } #endregion #region Header Actions protected abstract bool SelectColumns([NotNullWhen(true)] out DynamicGridColumns? columns); private void SelectColumnsClick() { if (SelectColumns(out var columns)) { VisibleColumns.Clear(); VisibleColumns.AddRange(columns); SaveColumns(columns); Refresh(true, true); } } #endregion #region Drag + Drop /// /// Handle a number of rows from a different being dragged into this one. /// /// The type of entity that that the rows of represent. /// The data being dragged. /// protected virtual void OnDragEnd(Type entity, CoreTable table, DragEventArgs e) { Logger.Send(LogType.Information,"","OnDragEnd"); } /// /// Handle all types of items being dragged onto this grid that aren't handled by , /// i.e., data which is not a from another /// /// /// Can be used to handle files, for example. /// /// /// protected virtual void HandleDragDrop(object sender, DragEventArgs e) { } protected virtual void HandleDragOver(object sender, DragEventArgs e) { } protected virtual DragDropEffects OnRowsDragStart(CoreRow[] rows) { return DragDropEffects.None; } #endregion } /// /// Shows that this can be used to import data. /// public interface IImportDynamicGrid { void DoImport(); } /// /// Shows that this can be used to export data. /// public interface IExportDynamicGrid { void DoExport(); } /// /// Shows that this can be used to duplicate data. /// public interface IDuplicateDynamicGrid { bool DoDuplicate(CoreRow[] rows); } /// /// Shows that this can show a help menu. /// public interface IHelpDynamicGrid { string HelpSlug(); }