using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Linq.Expressions; 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 InABox.Clients; using InABox.Core; using InABox.Wpf; using InABox.WPF; using Syncfusion.Data; using Syncfusion.UI.Xaml.Grid; using Syncfusion.UI.Xaml.Grid.Helpers; using static InABox.DynamicGrid.IDynamicGrid; using Color = System.Drawing.Color; using Columns = InABox.Core.Columns; using Image = System.Windows.Controls.Image; using RowColumnIndex = Syncfusion.UI.Xaml.ScrollAxis.RowColumnIndex; using SolidColorBrush = System.Windows.Media.SolidColorBrush; using String = System.String; using VerticalAlignment = System.Windows.VerticalAlignment; using VirtualizingCellsControl = Syncfusion.UI.Xaml.Grid.VirtualizingCellsControl; namespace InABox.DynamicGrid; public abstract class DynamicGrid : ContentControl { public static readonly DependencyProperty UseWaitCursorProperty = DependencyProperty.Register(nameof(UseWaitCursor), typeof(bool), typeof(DynamicGrid<>)); public bool UseWaitCursor { get => (bool)GetValue(UseWaitCursorProperty); set => SetValue(UseWaitCursorProperty, value); } } public abstract class DynamicGrid : DynamicGrid, IDynamicGridUIComponentParent, IDynamicGrid where T : BaseObject, new() { protected enum ClipAction { Cut, Copy } private IDynamicGridUIComponent UIComponent { get; set; } 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 Button Cut; private readonly Border Disabler; private readonly Button Delete; private readonly DockPanel Docker; private readonly DynamicRowMovementColumn? down; private readonly Button Edit; private readonly Label EditSpacer; private readonly Button Export; private readonly Label ExportSpacer; private readonly Button DuplicateBtn; private readonly Button SwitchViewBtn; private readonly Button Help; private readonly Button Import; private readonly Grid Layout; private readonly Label Loading; private DoubleAnimation LoadingFader = new DoubleAnimation(1d, 0.2d, new Duration(TimeSpan.FromSeconds(2))) { AutoReverse = true }; //private readonly Button MultiEdit; private readonly Button Paste; private readonly Button Print; private readonly Label PrintSpacer; private readonly StackPanel LeftButtonStack; private readonly StackPanel RightButtonStack; private readonly DynamicRowMovementColumn? up; protected DynamicGridRowStyleSelector RowStyleSelector; #region Events public event IDynamicGrid.ReconfigureEvent? OnReconfigure; public OnGetDynamicGridRowStyle? OnGetRowStyle { get; set; } public ValidateEvent? OnValidate { get; set; } public event OnPrintData? OnPrintData; public event BeforeRefreshEventHandler? BeforeRefresh; public event AfterRefreshEventHandler? AfterRefresh; public event OnDefineFilter? OnDefineFilter; public event OnCreateItem? OnCreateItem; public event OnAfterCreateItem? OnAfterCreateItem; /// /// 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 EditorValueChangedHandler? OnEditorValueChanged; public event OnCustomiseEditor? OnCustomiseEditor; public event OnFilterRecord? OnFilterRecord; public event OnDoubleClick? OnDoubleClick; public event EntitySaveEvent? OnBeforeSave; public event EntitySaveEvent? OnAfterSave; public delegate void EditorLoaded(IDynamicEditorForm editor, T[] items); public event EditorLoaded OnEditorLoaded; public event OnLoadEditorButtons OnLoadEditorButtons; public virtual event OnCustomiseColumns? OnCustomiseColumns; protected void DoCustomiseColumnsEvent(object sender, DynamicGridColumns columns) => OnCustomiseColumns?.Invoke(sender, columns); #endregion protected DynamicGridSettings Settings { get; set; } public DynamicGrid() : 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(); MasterColumns = new DynamicGridColumns(); MasterColumns.ExtractColumns(typeof(T)); HiddenColumns = new HiddenColumnsList(); foreach (var column in LookupFactory.RequiredColumns().ColumnNames()) { AddHiddenColumn(column); } if (ShowSequenceButtons) { up = new DynamicRowMovementColumn(DynamicRowMovement.Up, SwapRows); ActionColumns.Add(up); down = new DynamicRowMovementColumn(DynamicRowMovement.Down, SwapRows); ActionColumns.Add(down); HiddenColumns.Add(x => (x as ISequenceable)!.Sequence); } VisibleColumns = new DynamicGridColumns(); 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); } }; 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(typeof(T).Name.Split('.').Last().SplitCamelCase().Replace(" ", "_")); 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 }; Cut = CreateButton(Wpf.Resources.cut.AsBitmapImage(Color.White)); Cut.Margin = new Thickness(0, 2, 2, 0); Cut.Click += Cut_Click; Copy = CreateButton(Wpf.Resources.copy.AsBitmapImage(Color.White)); Copy.Margin = new Thickness(0, 2, 2, 0); Copy.Click += Copy_Click; Paste = CreateButton(Wpf.Resources.paste.AsBitmapImage(Color.White)); Paste.Margin = new Thickness(0, 2, 2, 0); Paste.Click += Paste_Click; ClipboardSpacer = new Label { Width = 5 }; Export = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Export"); Export.Margin = new Thickness(0, 2, 2, 0); Export.Click += Export_Click; Import = CreateButton(Wpf.Resources.doc_xls.AsBitmapImage(Color.White), "Import"); Import.Margin = new Thickness(0, 2, 2, 0); Import.Click += Import_Click; ExportSpacer = new Label { Width = 5 }; LeftButtonStack = new StackPanel(); LeftButtonStack.Orientation = Orientation.Horizontal; LeftButtonStack.SetValue(DockPanel.DockProperty, Dock.Left); 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(Cut); LeftButtonStack.Children.Add(Copy); LeftButtonStack.Children.Add(Paste); LeftButtonStack.Children.Add(ClipboardSpacer); LeftButtonStack.Children.Add(Export); LeftButtonStack.Children.Add(Import); 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; DuplicateBtn = AddButton("Duplicate", Wpf.Resources.paste.AsBitmapImage(Color.White), DoDuplicate); 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(); } #region IDynamicGridUIComponentParent protected virtual IDynamicGridUIComponent CreateUIComponent() { return new DynamicGridGridUIComponent() { Parent = this }; } bool IDynamicGridUIComponentParent.CanSort() { return !ShowSequenceButtons; } T IDynamicGrid.LoadItem(CoreRow row) => LoadItem(row); DynamicGridRowStyleSelector IDynamicGridUIComponentParent.RowStyleSelector => RowStyleSelector; void IDynamicGridUIComponentParent.BeforeSelection(CancelEventArgs cancel) { BeforeSelection(cancel); } void IDynamicGridUIComponentParent.SelectItems(CoreRow[] rows) { SelectItems(rows); } void IDynamicGridUIComponentParent.EntityChanged(T obj, CoreRow row, string changedColumn, Dictionary changes) => EntityChanged(obj, row, changedColumn, changes); void IDynamicGridUIComponentParent.UpdateData(T obj, CoreRow row, string changedColumn, Dictionary updates) { var result = new Dictionary(); foreach (var (col, value) in updates) { UpdateRow(row, col.ColumnName, value, refresh: false); DynamicGridUtils.UpdateEditorValue(new BaseObject[] { obj }, col.ColumnName, value, result); } EntityChanged(obj, row, changedColumn, result); } void IDynamicGridUIComponentParent.HandleKey(KeyEventArgs e) { if (ShowSequenceButtons) { 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 && column is DynamicGridColumn col) { 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 = string.Format("{0} Records", count); } 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); } void IDynamicGridUIComponentParent.UIFilterChanged(object sender) => DoFilterChanged(); protected virtual void DoFilterChanged() { } IEnumerable? IDynamicGridUIComponentParent.GetColumnFilterItems(DynamicColumnBase column) => GetColumnFilterItems(column); protected virtual IEnumerable? GetColumnFilterItems(DynamicColumnBase column) { return null; } #endregion protected virtual DynamicGridRowStyleSelector GetRowStyleSelector() { return new DynamicGridRowStyleSelector(); } 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 data in the grid. This is until is called. /// public CoreTable? MasterData { get; set; } public DynamicGridColumns MasterColumns { get; protected set; } public DynamicGridColumns VisibleColumns { get; protected set; } public DynamicActionColumns ActionColumns { get; protected set; } public CoreTable Data { get; set; } public class HiddenColumnsList { private List Columns { get; set; } = new(); public IEnumerable ColumnNames => Columns; public void Add(Expression> column) => Add(CoreUtils.GetFullPropertyName(column, ".")); public void Add(IColumn column) => Add(column.Property); public void Add(string column) { if (!Contains(column)) Columns.Add(column); } public bool Contains(string column) => Columns.Contains(column); } public void AddHiddenColumn(string column) => HiddenColumns.Add(column); public HiddenColumnsList HiddenColumns { get; } private static bool IsSequenced => typeof(T).GetInterfaces().Any(x => x.Equals(typeof(ISequenceable))); protected virtual bool ShowSequenceButtons => IsSequenced; 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; 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; Cut.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed; Copy.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed; Paste.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed; ClipboardSpacer.Visibility = ShowSequenceButtons && Options.EditRows ? Visibility.Visible : Visibility.Collapsed; Export.Visibility = Options.ExportData ? Visibility.Visible : Visibility.Collapsed; Import.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 (up != null) up.Position = Options.EditRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden; if (down != null) down.Position = Options.EditRows ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden; if (DuplicateBtn != null) DuplicateBtn.Visibility = Visibility.Collapsed; reloadColumns = reloadColumns || UIComponent.OptionsChanged(); if(reloadColumns) { 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(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); } private void EntityChanged(T obj, CoreRow row, string changedColumn, Dictionary changes) { OnAfterEditorValueChanged(null, [obj], new AfterEditorValueChangedArgs(changedColumn, changes), changes); SaveItem(obj); foreach (var (key, value) in changes) { row[key] = value; } UIComponent.UpdateRow(row); } #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)); DuplicateBtn.Visibility = typeof(T).IsAssignableTo(typeof(IDuplicatable)) && 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 virtual DynamicGridColumns GenerateColumns() { var columns = new DynamicGridColumns(); var cols = IsDirectEditMode() ? new Columns(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys) : new Columns(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked); foreach (var col in cols) { var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property)); if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden) columns.Add(mc); } return columns; } private bool SwapRows(int row1, int row2) { CoreRow[] rows = Data.Rows.Where(x => x.Index.Equals(row1) || x.Index.Equals(row2)).ToArray(); var items = LoadItems(rows); var first = (items.First() as ISequenceable)!; var last = (items.Last() as ISequenceable)!; var iBuf1 = first.Sequence; var iBuf2 = last.Sequence; first.Sequence = iBuf2; last.Sequence = iBuf1; SaveItems(items); return true; } protected virtual void SaveColumns(DynamicGridColumns columns) { } public int DesiredWidth() { return UIComponent.DesiredWidth(); } public virtual void ConfigureColumns(DynamicGridColumns columns) { DoCustomiseColumnsEvent(this,columns); } /// /// 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() { } private void ReloadColumns() { ConfigureColumns(MasterColumns /*, false */); VisibleColumns = LoadColumns(); ConfigureColumns(VisibleColumns /*, true */); ColumnGroupings.Clear(); ConfigureColumnGroups(); UIComponent.RefreshColumns(VisibleColumns, ActionColumns, ColumnGroupings); } #endregion #region Refresh / Reload protected abstract void Reload(Filters criteria, Columns columns, ref SortOrder? sort, Action action); public Filter? DefineFilter() { if (OnDefineFilter is null) return null; var result = OnDefineFilter.Invoke(typeof(T)) as Filter; return result; } protected virtual bool FilterRecord(CoreRow row) { var bOK = ActionColumns.All(x => { return x.FilterRecord is null || x.SelectedFilters is null || x.SelectedFilters.Length == 0 || x.FilterRecord.Invoke(row, x.SelectedFilters); }); if (bOK && OnFilterRecord is not null) bOK = OnFilterRecord(row); return bOK; } public IEnumerable ExtractValues(Expression> column, Selection selection) { var result = selection == Selection.None ? Enumerable.Empty() : selection == Selection.Selected ? SelectedRows.Select(r => r.Get(column)) : Data.ExtractValues(column); return result; } public virtual void Refresh(bool reloadcolumns, bool reloaddata) { if (bRefreshing) return; if (!DoBeforeRefresh()) return; UIComponent.BeforeRefresh(); var cursor = UseWaitCursor ? new WaitCursor() : null; Loading.Visibility = Visibility.Visible; Loading.BeginAnimation(Label.OpacityProperty, LoadingFader); bRefreshing = true; if (reloadcolumns) ReloadColumns(); if (reloaddata) { _lookupcache.Clear(); var criteria = new Filters(); var filter = DefineFilter(); if (filter != null) criteria.Add(filter); var columns = DataColumns(); var sort = LookupFactory.DefineSort(); if (sort == null && IsSequenced) sort = new SortOrder("Sequence"); Reload( criteria , columns , ref sort , (table, exception) => { if (exception != null) { Dispatcher.Invoke(() => { MessageWindow.ShowError("Sorry! We couldn't load the data.", exception); }); } else if (table is not null) { MasterData = table; Dispatcher.Invoke(() => { ProcessData(); DoAfterRefresh(); bRefreshing = false; IsReady = true; }); } } ); } else { ProcessData(); DoAfterRefresh(); bRefreshing = false; IsReady = true; Loading.BeginAnimation(Label.OpacityProperty, null); Loading.Visibility = Visibility.Collapsed; } if (cursor != null) { cursor.Dispose(); cursor = null; } } 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()); } public Columns DataColumns() { var columns = Columns.None(); foreach (var column in VisibleColumns) columns.Add(column.ColumnName); foreach (var column in HiddenColumns.ColumnNames) columns.Add(new Column(column)); return columns; } private void ProcessData() { Data.Columns.Clear(); Data.Setters.Clear(); if (MasterData != null) foreach (var column in MasterData.Columns) Data.Columns.Add(column); LoadData(); } 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); } public void UpdateRow(CoreRow row, T obj) { ObjectToRow(obj, row); ObjectToRow(obj, _recordmap[row]); } public void AddRow(CoreRow row) { if (MasterData is null) return; var masterrow = MasterData.NewRow(); MasterData.FillRow(masterrow, row); Refresh(false, false); } public void AddRow(T data) { if (MasterData is null) return; MasterData.LoadRow(data); Refresh(false, false); } public void DeleteRow(CoreRow row) { if (MasterData is null) return; var masterrow = _recordmap[row]; MasterData.Rows.Remove(masterrow); Refresh(false, false); } private void FilterRows(CoreTable from, CoreTable into, Dictionary? recordMap = null, Func? filter = null) { into.Rows.Clear(); recordMap?.Clear(); foreach (var row in from.Rows.ToArray()) 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; //else if (Data.Columns[i].DataType == typeof(String[])) // value = String.Join("\n", value as String[]); newrow.Values.Add(value); } //newrow.Values.AddRange(row.Values); //if ((OnFilterRecord == null) || (OnFilterRecord(row))) into.Rows.Add(newrow); recordMap?.TryAdd(newrow, row); } } private void LoadData() { ResetClipBuffer(); if (MasterData is null) return; FilterRows(MasterData, Data, _recordmap); InvalidateGrid(); //ScrollBar.Value = _CurrentRow <= 0 ? 0 : _CurrentRow; SelectedRows = Array.Empty(); } //IncrementalList _data = null; 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(); } #endregion #region Item Manipulation #region Load/Save/Delete public virtual T[] LoadItems(CoreRow[] rows) { var result = new List(); foreach (var row in rows) { var index = Data.Rows.IndexOf(row); result.Add(LoadItem(row)); } return result.ToArray(); } public abstract T LoadItem(CoreRow row); public abstract void SaveItem(T item); public virtual void SaveItems(T[] items) { foreach (var item in items) SaveItem(item); } protected virtual bool CanDeleteItems(params CoreRow[] rows) { return true; } public abstract void DeleteItems(params CoreRow[] rows); protected virtual void DoDelete() { var rows = SelectedRows.ToArray(); if (rows.Any()) if (CanDeleteItems(rows)) if (MessageBox.Show("Are you sure you wish to delete the selected records?", "Confirm Delete", MessageBoxButton.YesNo) == MessageBoxResult.Yes) { DeleteItems(rows); SelectedRows = Array.Empty(); OnChanged?.Invoke(this, EventArgs.Empty); Refresh(false, true); 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) { //CoreRow row = (SelectedRow > -1) && (SelectedRow < Data.Rows.Count) ? Data.Rows[this.SelectedRow] : null; if (IsDirectEditMode() && !OpenEditorOnDirectEdit) { CreateItems(null); } else if (AddEditClick(null)) { Refresh(false, true); } } protected void CreateItems(Func>? create) { var newRows = new List(); var items = create?.Invoke() ?? CoreUtils.One(CreateItem()); foreach (var item in items) { if (!AfterCreate(item)) return; SaveItem(item); var datarow = Data.NewRow(); ObjectToRow(item, datarow); Data.Rows.Add(datarow); newRows.Add(datarow); var masterrow = MasterData.NewRow(); ObjectToRow(item, masterrow); MasterData.Rows.Add(masterrow); _recordmap[datarow] = masterrow; } InvalidateGrid(); SelectedRows = newRows.ToArray(); OnChanged?.Invoke(this, EventArgs.Empty); } private void Add_Click(object sender, RoutedEventArgs e) { if (CanCreateItems()) DoAdd(); } public virtual DynamicEditorPages LoadEditorPages(T item) { DynamicEditorPages pages = new DynamicEditorPages(); DynamicGridUtils.LoadOneToManyPages(typeof(T), pages); DynamicGridUtils.LoadEnclosedListPages(typeof(T), pages); DynamicGridUtils.LoadManyToManyPages(typeof(T), pages); DynamicGridUtils.LoadCustomEditorPages(typeof(T), pages); foreach (var page in pages) page.Ready = false; return pages; } public virtual void LoadEditorButtons(T item, DynamicEditorButtons buttons) { buttons.Clear(); buttons.Add( "", Wpf.Resources.help.AsBitmapImage(), item, (f, i) => { Process.Start(new ProcessStartInfo("https://prsdigital.com.au/wiki/index.php/" + typeof(T).Name.SplitCamelCase().Replace(" ", "_")) { UseShellExecute = true }); } ); OnLoadEditorButtons?.Invoke(item, buttons); } protected virtual void BeforeLoad(IDynamicEditorForm form, T[] items) { form.BeforeLoad(); } void IDynamicGrid.InitialiseEditorForm(IDynamicEditorForm editor, object[] items, Func? pageDataHandler, bool preloadPages) { InitialiseEditorForm(editor, items.Cast().ToArray(), pageDataHandler, preloadPages); } public virtual bool EditItems(object[] items, Func? PageDataHandler = null, bool PreloadPages = false) { var values = items.Cast().ToArray(); return EditItems(values, PageDataHandler, PreloadPages); } public virtual void InitialiseEditorForm(IDynamicEditorForm editor, T[] items, Func? pageDataHandler = null, bool preloadPages = false) { var pages = items.Length == 1 ? LoadEditorPages(items.First()) : new DynamicEditorPages(); var buttons = new DynamicEditorButtons(); if (items.Length == 1) LoadEditorButtons(items.First(), buttons); editor.Setup(items.Any() ? items.First().GetType() : typeof(T), pages, buttons, pageDataHandler, preloadPages); editor.OnCustomiseColumns = (sender, columns) => { columns.Clear(); columns.AddRange(MasterColumns); ConfigureColumns(columns); }; editor.OnDefineEditor = (o, c) => { var result = GetEditor(o, c); if (result != null) result = result.CloneEditor(); return result; }; editor.OnFormCustomiseEditor += DoCustomiseEditor; editor.OnDefineFilter = (type, column) => { return DefineLookupFilter(type, column, items); }; //editor.OnDefineFilter += (o, e) => { return DefineFilter(items, e); }; editor.OnDefineLookups = editor => DefineLookups(editor, items); editor.OnEditorValueChanged += (s, n, v) => EditorValueChanged(editor, items, n, v); editor.OnAfterEditorValueChanged += (g, args) => AfterEditorValueChanged(g, items, args); editor.OnReconfigureEditors = g => DoReconfigureEditors(g, items); editor.OnValidateData += (o, i) => ValidateData(items); editor.OnSelectPage += SelectPage; editor.OnSaveItem = (o, e) => { try { using var Wait = new WaitCursor(); DoBeforeSave(editor, items); if (items.Length == 1) editor.UnloadEditorPages(false); foreach (var item in items) SaveItem(item); if (items.Length == 1) editor.UnloadEditorPages(true); DoAfterSave(editor, items); } catch (Exception err) { MessageBox.Show(err.Message); e.Cancel = true; } }; BeforeLoad(editor, items); editor.Items = items; AfterLoad(editor, items); } private void DoCustomiseEditor(IDynamicEditorForm sender, object[] items, DynamicGridColumn column, BaseEditor editor) { CustomiseEditor((T[])items, column, editor); OnCustomiseEditor?.Invoke(sender, (T[])items, column, editor); } protected virtual void CustomiseEditor(T[] items, DynamicGridColumn column, BaseEditor editor) { } protected virtual void DoAfterSave(IDynamicEditorForm editor, T[] items) { OnAfterSave?.Invoke(editor, items); } protected virtual void DoBeforeSave(IDynamicEditorForm editor, T[] items) { OnBeforeSave?.Invoke(editor, items); } public virtual bool EditItems(T[] items, Func? PageDataHandler = null, bool PreloadPages = false) { DynamicEditorForm editor; using (var cursor = new WaitCursor()) { editor = new DynamicEditorForm(); editor.SetValue(Panel.ZIndexProperty, 999); InitialiseEditorForm(editor, items, PageDataHandler, PreloadPages); OnEditorLoaded?.Invoke(editor, items); } return editor.ShowDialog() == true; } private Dictionary AfterEditorValueChanged(DynamicEditorGrid grid, T[] items, AfterEditorValueChangedArgs args) { var changes = new Dictionary(); OnAfterEditorValueChanged(grid, items, args, changes); return changes; } protected virtual void OnAfterEditorValueChanged(DynamicEditorGrid? grid, T[] items, AfterEditorValueChangedArgs args, Dictionary changes) { } protected virtual void DoReconfigureEditors(DynamicEditorGrid grid, T[] items) { /*if (items.First() is IDimensioned dimensioned) { UpdateEditor(grid, x => x.Dimensions.Quantity, dimensioned.Dimensions.GetUnit().HasQuantity); UpdateEditor(grid, x => x.Dimensions.Length, dimensioned.Dimensions.GetUnit().HasLength); UpdateEditor(grid, x => x.Dimensions.Width, dimensioned.Dimensions.GetUnit().HasWidth); UpdateEditor(grid, x => x.Dimensions.Height, dimensioned.Dimensions.GetUnit().HasHeight); UpdateEditor(grid, x => x.Dimensions.Weight, dimensioned.Dimensions.GetUnit().HasWeight); }*/ } private List? ValidateData(T[] items) { var errors = new List(); DoValidate(items, errors); OnValidate?.Invoke(this, items, errors); return errors.Count != 0 ? errors : null; } protected virtual void DoValidate(T[] items, List errors) { } protected virtual void AfterLoad(IDynamicEditorForm editor, T[] items) { editor.AfterLoad(); } protected virtual void SelectPage(object sender, BaseObject[]? items) { } protected virtual Dictionary EditorValueChanged(IDynamicEditorForm editor, T[] items, string name, object value) { var result = DynamicGridUtils.UpdateEditorValue(items, name, value); if (OnEditorValueChanged != null) { var newchanges = OnEditorValueChanged(editor, name, value); foreach (var key in newchanges.Keys) result[key] = newchanges[key]; } return result; } private readonly Dictionary, Dictionary> _lookupcache = new(); protected virtual void DefineLookups(ILookupEditorControl sender, T[] items, bool async = true) { if (sender.EditorDefinition is not ILookupEditor editor) return; var colname = sender.ColumnName; if (async) { Task.Run(() => { try { var values = editor.Values(typeof(T), colname, items); Dispatcher.Invoke( () => { try { //Logger.Send(LogType.Information, typeof(T).Name, "Dispatching Results" + colname); sender.LoadLookups(values); } catch (Exception e2) { Logger.Send(LogType.Information, typeof(T).Name, "Exception (2) in LoadLookups: " + e2.Message + "\n" + e2.StackTrace); } } ); } catch (Exception e) { Logger.Send(LogType.Information, typeof(T).Name, "Exception (1) in LoadLookups: " + e.Message + "\n" + e.StackTrace); } }); } else { var values = editor.Values(typeof(T), colname, items); sender.LoadLookups(values); } } /// /// Retrieves an editor to display for the given column of . /// /// The object being edited. /// The column of the editor. /// A new editor, or if no editor defined and no sensible default exists. protected virtual BaseEditor? GetEditor(object item, DynamicGridColumn column) { return column.Editor ?? CoreUtils.GetProperty(item.GetType(), column.ColumnName).GetEditor(); } protected IFilter? DefineLookupFilter(Type type, string column, T[] items) { return LookupFactory.DefineLookupFilter(typeof(T), type, column, items); } protected virtual void SetEditorValue(object item, string name, object value) { try { CoreUtils.SetPropertyValue(item, name, value); } catch (Exception e) { Logger.Send(LogType.Error, "", string.Format("*** Unknown Error: {0}\n{1}", e.Message, e.StackTrace)); } } protected virtual object? GetEditorValue(object item, string name) { return CoreUtils.GetPropertyValue(item, name); } protected virtual bool CanCreateItems() { return true; } private bool AddEditClick(CoreRow[]? rows) { if (!IsEnabled || bRefreshing) return false; if (rows == null || rows.Length == 0) { if (!CanCreateItems()) return false; var item = CreateItem(); if (!AfterCreate(item)) return false; // Yea, and this won't work, because we're actually usually showing the description of a linked item, // not the id of the link, and we need to set the ID to have it work properly :-( //foreach (String key in VisualFilters.Keys) // CoreUtils.SetPropertyValue(item, key, VisualFilters[key]); if (EditItems([item])) { //_CurrentRow = Data.Rows.Count; var row = Data.NewRow(); ObjectToRow(item, row); Data.Rows.Add(row); InvalidateGrid(); SelectedRows = [row]; DoChanged(); return true; } return false; } var items = Array.Empty(); using (new WaitCursor()) { Stopwatch sw = new Stopwatch(); sw.Start(); items = LoadItems(rows); //Logger.Send(LogType.Information, "DG:LoadItems", String.Format("Loaded Items: {0}ms", sw.ElapsedMilliseconds)); sw.Stop(); } if (items.Length != 0) { var sel = SelectedRows; if (EditItems(items)) { for (var i = 0; i < items.Length; i++) { UpdateRow(rows[i], items[i]); } InvalidateGrid(); SelectedRows = sel; DoChanged(); return true; } return false; } return false; } #endregion #region Duplicate protected virtual IEnumerable LoadDuplicatorItems(CoreRow[] rows) { return LoadItems(rows); } private bool DoDuplicate(Button button, CoreRow[] rows) { if (!rows.Any()) { MessageBox.Show("Please select at least one record to duplicate!"); return false; } /*var ids = ExtractValues(x => x.ID, Selection.Selected).ToArray(); if (!ids.Any()) { MessageBox.Show("Please select at least one record to duplicate!"); return false; }*/ var duplicator = (new T() as IDuplicatable)?.GetDuplicator(); if (duplicator is null) { MessageBox.Show($"Cannot duplicate {typeof(T)}"); return false; } duplicator.Duplicate(LoadDuplicatorItems(rows));// new Filter(x => x.ID).InList(ids)); return true; } #endregion public virtual T CreateItem() { var result = new T(); OnCreateItem?.Invoke(this, result); return result; } public virtual bool AfterCreate(T item) { return OnAfterCreateItem?.Invoke(this, item) ?? true; } 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 }); } protected void ReloadForms(IDynamicEditorForm editor, TTargetType item, Expression> sourcekey, Guid sourceid) where TTargetType : Entity, new() where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new() where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm, new() { var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm)); var page = editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid; if (page != null && item != null) { if (!page.Ready) page.Load(item, null); CoreTable table; if (sourceid == Guid.Empty) { table = new CoreTable(); table.LoadColumns(typeof(TSourceForm)); } else { table = new Client().Query( new Filter(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo) .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last()) ); } var newforms = new List(); foreach (var row in table.Rows) { var sourceform = row.ToObject(); var targetform = new TTargetForm(); targetform.Form.ID = sourceform.Form.ID; targetform.Form.Synchronise(sourceform.Form); newforms.Add(targetform); } page.Items.Clear(); page.LoadItems(newforms.ToArray()); } } #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; if (!ShowSequenceButtons) return; using (new WaitCursor()) { var updates = ClipBuffer.Item2.Select(x => x.ToObject()).ToList(); if (BeforePaste(updates, ClipBuffer.Item1)) { var currow = SelectedRows.FirstOrDefault() ?? Data.Rows.LastOrDefault(); var sequence = currow != null ? currow.Get(c => ((ISequenceable)c).Sequence) : 0; var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get(x => x.Sequence) >= sequence); updates.AddRange(LoadItems(postrows.ToArray())); foreach (var update in updates) { sequence++; ((ISequenceable)update).Sequence = sequence; } } if (updates.Any()) { SaveItems(updates.ToArray()); Refresh(false, true); } } } protected virtual bool BeforePaste(IEnumerable items, ClipAction action) { return true; } private void Cut_Click(object sender, RoutedEventArgs e) { CutToClipBuffer(); } private void Copy_Click(object sender, RoutedEventArgs e) { CopyToClipBuffer(); } private void Paste_Click(object sender, RoutedEventArgs e) { PasteFromClipBuffer(); } #endregion protected virtual void ObjectToRow(T obj, CoreRow row) { row.Table.FillRow(row, obj); } #region Import / Export protected virtual CoreTable LoadImportKeys(String[] fields) { var result = new CoreTable(); result.LoadColumns(Columns.None().Add(fields)); return result; } protected virtual Guid GetImportID() { return Guid.Empty; } protected virtual bool CustomiseImportItem(T item) { if (IsSequenced) ((ISequenceable)item).Sequence = CoreUtils.GenerateSequence(); return true; } protected virtual string CustomiseImportFileName(string filename) { return filename; } protected virtual void DoImport() { var list = new DynamicImportList( typeof(T), GetImportID() ); list.OnImportItem += o => { return CustomiseImportItem((T)o); }; list.OnCustomiseImport += (o, args) => { args.FileName = CustomiseImportFileName(args.FileName); }; list.OnSave += (sender, entity) => SaveItem(entity as T); list.OnLoad += (sender, type, fields, id) => LoadImportKeys(fields); list.ShowDialog(); Refresh(false, true); } private void Import_Click(object sender, RoutedEventArgs e) { DoImport(); } protected virtual void CustomiseExportColumns(List columnnames) { } protected virtual string CustomiseExportFileName(string filename) { return filename; } protected virtual void CustomiseExportFilters(Filters filters, CoreRow[] visiblerows) { } protected virtual void ApplyExportFilter(CoreTable table, object data) { } protected virtual void DoExport() { var columnnames = VisibleColumns.Select(x => x.ColumnName).ToList(); CustomiseExportColumns(columnnames); var form = new DynamicExportForm(typeof(T), columnnames); if (form.ShowDialog() != true) return; var filters = new Filters(); filters.Add(DefineFilter()); var predicates = UIComponent.GetFilterPredicates(); var visiblerows = GetVisibleRows(); CustomiseExportFilters(filters, visiblerows); var columns = Columns.None().Add(form.Fields); var otherColumns = form.GetChildFields() .Select(x => new Tuple( x.Key, Columns.None(x.Key).Add(x.Value))) .Where(x => x.Item2.ColumnNames().Any()).ToList(); var reloadColumns = Columns.None(); foreach (var column in columns) { reloadColumns.Add(column); } foreach (var column in HiddenColumns.ColumnNames) { reloadColumns.Add(column); } foreach (var (column, _) in predicates) { reloadColumns.Add(column); } var sort = LookupFactory.DefineSort(); Reload(filters, reloadColumns, ref sort, (data, err) => Dispatcher.Invoke(() => { if (data is not null) { var newData = new CoreTable(); newData.LoadColumns(columns); FilterRows(data, newData, filter: row => { foreach(var (_, predicate) in predicates) { if (!predicate(row)) { return false; } } return true; }); var list = new List>() { new(typeof(T), newData) }; list.AddRange(LoadExportTables(filters, otherColumns)); DoExportTables(list); } else if (err is not null) { Logger.Send(LogType.Error, "", $"Error in export: {CoreUtils.FormatException(err)}"); MessageBox.Show(err.Message); } })); } private void Export_Click(object sender, RoutedEventArgs e) { DoExport(); } /// /// Loads the child tables for an export, based on the filter of the parent table. /// /// /// If not overriden, defaults to creating empty tables with no records. /// /// Filter for the parent table. /// A list of the child table types, with columns to load for each /// A list of tables, in the same order as they came in protected virtual IEnumerable> LoadExportTables(Filters filter, IEnumerable> tableColumns) { return tableColumns.Select(x => { var table = new CoreTable(); table.LoadColumns(x.Item2); return new Tuple(x.Item1, table); }); } private void DoExportTables(List> data) { var filename = CustomiseExportFileName(typeof(T).EntityName().Split('.').Last()); ExcelExporter.DoExport(data, filename); } #endregion public void ScrollIntoView(CoreRow row) { UIComponent.ScrollIntoView(row); } #endregion #region Custom Buttons private Button CreateButton(BitmapImage? 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; UpdateButton(button, image, text, tooltip); return button; } public void UpdateButton(Button button, BitmapImage? 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 (Export.Visibility != Visibility.Collapsed) return true; return false; } public Button AddButton(string? caption, BitmapImage? 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, BitmapImage? 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 private void SelectColumnsClick() { var editor = new DynamicGridColumnsEditor(typeof(T)); editor.DirectEdit = IsDirectEditMode(); editor.Columns.AddRange(VisibleColumns); if (editor.ShowDialog().Equals(true)) { VisibleColumns.Clear(); VisibleColumns.AddRange(editor.Columns); SaveColumns(VisibleColumns); //OnSaveColumns?.Invoke(this, editor.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 DragDropEffects DragTable(Type entity, CoreTable table) { Logger.Send(LogType.Information, "", "DragTable"); var data = new DataObject(); data.SetData(DynamicGridUtils.DragFormat, new DynamicGridDragFormat(table.ToDataTable(), entity)); var effect = DragDrop.DoDragDrop(this, data, DragDropEffects.All); return effect; } protected virtual DragDropEffects OnRowsDragStart(CoreRow[] rows) { Logger.Send(LogType.Information, "", "OnRowsDragStart"); var table = new CoreTable(); table.LoadColumns(Data.Columns); table.LoadRows(rows); return DragTable(typeof(T), table); } #endregion } #region Styling public class DynamicGridRowStyle : DynamicGridStyle { public DynamicGridRowStyle() : base(null) { } public DynamicGridRowStyle(IDynamicGridStyle source) : base(source) { } public override DependencyProperty FontSizeProperty => Control.FontSizeProperty; public override DependencyProperty FontStyleProperty => Control.FontStyleProperty; public override DependencyProperty FontWeightProperty => Control.FontWeightProperty; public override DependencyProperty BackgroundProperty => Control.BackgroundProperty; public override DependencyProperty ForegroundProperty => Control.ForegroundProperty; } public class DynamicGridCellStyle : DynamicGridStyle { public DynamicGridCellStyle() : base(null) { } public DynamicGridCellStyle(IDynamicGridStyle source) : base(source) { } public override DependencyProperty FontSizeProperty => Control.FontSizeProperty; public override DependencyProperty FontStyleProperty => Control.FontStyleProperty; public override DependencyProperty FontWeightProperty => Control.FontWeightProperty; public override DependencyProperty BackgroundProperty => Control.BackgroundProperty; public override DependencyProperty ForegroundProperty => Control.ForegroundProperty; } public class GridSelectionControllerExt : GridSelectionController { public GridSelectionControllerExt(SfDataGrid datagrid) : base(datagrid) { } protected override void ProcessSelectedItemChanged(SelectionPropertyChangedHandlerArgs handle) { base.ProcessSelectedItemChanged(handle); if (handle.NewValue != null) { //this.DataGrid.ScrollInView(this.CurrentCellManager.CurrentRowColumnIndex); //int rowIndex = this.CurrentCellManager.CurrentRowColumnIndex.RowIndex; var columnIndex = CurrentCellManager.CurrentRowColumnIndex.ColumnIndex; var scrollRowIndex = DataGrid.GetVisualContainer().ScrollRows.LastBodyVisibleLineIndex; DataGrid.ScrollInView(new RowColumnIndex(scrollRowIndex, columnIndex)); } } } // Used to render boolean columns (the default "false" value shows what appears to be an intermediate state, which is ugly // This should show nothing for false, and a tick in a box for true public class BoolToImageConverter : AbstractConverter { public ImageSource TrueValue { get; set; } public ImageSource FalseValue { get; set; } public BoolToImageConverter() { TrueValue = Wpf.Resources.Bullet_Tick.AsBitmapImage(); } public override ImageSource Convert(bool value) { return value ? TrueValue : FalseValue; } public override bool Deconvert(ImageSource value) { return ImageUtils.IsEqual(value as BitmapImage,TrueValue as BitmapImage); } } public class StringToColorImageConverter : IValueConverter { private readonly int _height = 50; private readonly int _width = 25; private readonly Dictionary cache = new(); public StringToColorImageConverter(int width, int height) { _width = width; _height = height; } public object? Convert(object value, Type targetType, object parameter, CultureInfo culture) { var str = value?.ToString(); if (str is null) return null; var colorcode = str.TrimStart('#'); if (!cache.ContainsKey(colorcode)) { var col = ImageUtils.StringToColor(colorcode); var bmp = ImageUtils.BitmapFromColor(col, _width, _height, Color.Black); cache[colorcode] = bmp.AsBitmapImage(); } var result = cache[colorcode]; return result; } public object? ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return null; } } public class StringArrayConverter : IValueConverter { object? IValueConverter.Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value is string[] strArray) { return string.Join("\n", strArray); } Logger.Send(LogType.Error, "", $"Attempt to convert an object which is not a string array: {value}."); return null; } object IValueConverter.ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { return value; } } #endregion [Serializable] class DynamicGridDragFormat { private string entity; public DataTable Table { get; set; } public Type Entity { get => CoreUtils.GetEntity(entity); set => entity = value.EntityName(); } public DynamicGridDragFormat(DataTable table, Type entity) { Table = table; Entity = entity; } }