| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743 | 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;    /// <summary>    /// Called when an item is selected in the grid. It is not called if <see cref="IsReady"/> is not <see langword="true"/>.    /// </summary>    /// <remarks>    /// It is unnecessary to use this if within a grid. Instead, override <see cref="SelectItems(CoreRow[]?)"/>.    /// </remarks>    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()    {    }    #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<T>.UIFilterChanged(object sender) => DoFilterChanged();    protected virtual void DoFilterChanged()    {            }    private Dictionary<DynamicColumnBase, IDynamicGridColumnFilter?> 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<DynamicGridRowStyle>();    }    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);            }        }    }    /// <summary>    /// Represents the unfiltered data in the grid. This is <see langword="null"/> until <see cref="Refresh(bool, bool)"/> is called.    /// </summary>    /// <remarks>    /// This differs from <see cref="Data"/> in that <see cref="Data"/> has been filtered by <see cref="FilterRecord(CoreRow)"/>,    /// whereas <see cref="MasterData"/> contains every record loaded from the database.    /// </remarks>    public CoreTable? MasterData { get; set; }    public DynamicGridColumns VisibleColumns { get; protected set; }    public DynamicActionColumns ActionColumns { get; protected set; }    private List<DynamicColumnBase> ColumnList = new();    IList<DynamicColumnBase> 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    /// <summary>    /// Initialise things like custom buttons; called once during construction.    /// </summary>    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);    }    /// <summary>    /// Configure custom buttons and options.    /// </summary>    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;    }    /// <summary>    /// Call the <see cref="OnSelectItem"/> event, and do any updating which needs to occur when items are selected.    /// </summary>    /// <param name="rows"></param>    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; }    /// <summary>    /// Create a new column header group, and return it for editing.    /// </summary>    /// <returns></returns>    public DynamicGridColumnGrouping AddColumnGrouping()    {        var group = new DynamicGridColumnGrouping();        ColumnGroupings.Add(group);        return group;    }    /// <summary>    /// Gets the current column header group, and if there is none, create a new one.    /// </summary>    /// <returns></returns>    public DynamicGridColumnGrouping GetColumnGrouping()    {        if(ColumnGroupings.Count == 0)        {            return AddColumnGrouping();        }        return ColumnGroupings[^1];    }    #endregion        protected virtual DynamicGridColumns LoadColumns()    {        return GenerateColumns();    }    /// <summary>    /// Provide a set of columns which is the default for this grid.    /// </summary>    public abstract DynamicGridColumns GenerateColumns();    protected abstract void SaveColumns(DynamicGridColumns columns);    public int DesiredWidth()    {        return UIComponent.DesiredWidth();    }    /// <summary>    /// Handle to configure column groups.    /// </summary>    /// <remarks>    /// This is called after <see cref="LoadColumns"/>, so by the time this is called, both <see cref="VisibleColumns"/>    /// and <see cref="ActionColumns"/> will be loaded, which means one can reference these in the column groups.    /// <br/>    /// <b>Note:</b> <see cref="ColumnGroupings"/> is cleared before this function is called.    /// </remarks>    protected virtual void ConfigureColumnGroups()    {    }    protected virtual void ConfigureColumns(DynamicGridColumns columns)    {    }    public class ColumnsLoadedEventArgs : EventArgs    {        public List<DynamicColumnBase> Columns { get; private set; }        public DynamicGridColumnGroupings ColumnGroupings { get; private set; }        public IEnumerable<DynamicActionColumn> ActionColumns => Columns.OfType<DynamicActionColumn>();        public IEnumerable<DynamicGridColumn> DataColumns => Columns.OfType<DynamicGridColumn>();        public ColumnsLoadedEventArgs(List<DynamicColumnBase> columns, DynamicGridColumnGroupings columnGroupings)        {            Columns = columns;            ColumnGroupings = columnGroupings;        }        public DynamicGridColumn Add<T>(            Expression<Func<T, object?>> 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<DynamicColumnBase> 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<DynamicColumnBase>();        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<DynamicGridColumn>());                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<CoreTable?, Exception?> 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());    }    /// <summary>    /// Process the data from <see cref="MasterData"/> according to <paramref name="range"/>.    /// </summary>    /// <remarks>    /// Set <paramref name="range"/> to <see langword="null"/> 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 <paramref name="range"/> is provided, this will add to the grid the rows    /// according to the range from <see cref="MasterData"/>.    /// </remarks>    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<CoreRow, CoreRow> _recordmap = new();    public void UpdateRow<TRow, TType>(CoreRow row, Expression<Func<TRow, TType>> column, TType value, bool refresh = true)    {        row.Set(column, value);        _recordmap[row].Set(column, value);        if (refresh)            InvalidateRow(row);    }    public void UpdateRow<TType>(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<CoreColumn, object?> updates)    {        var result = new Dictionary<string, object?>();        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);    }    /// <summary>    /// Filter all given rows into <paramref name="into"/>, given that they match <paramref name="filter"/> and <see cref="FilterRecord(CoreRow)"/>.    /// If <paramref name="recordMap"/> is given, also updates the map from <paramref name="from"/> to <paramref name="into"/>.    /// </summary>    protected IList<CoreRow> FilterRows(        IEnumerable<CoreRow> from,        CoreTable into,        Dictionary<CoreRow, CoreRow>? recordMap = null,        Func<CoreRow, bool>? filter = null)    {        var newRows = new List<CoreRow>();        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<CoreRow>();        }        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<Tuple<string, Func<CoreRow, bool>>> 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    /// <summary>    /// Create a new row.    /// </summary>    protected abstract void NewRow();    /// <summary>    /// Edit <paramref name="rows"/> 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.    /// </summary>    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<CoreRow>();                    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<ClipAction, CoreRow[]>? ClipBuffer;    protected void ResetClipBuffer()    {        ClipBuffer = null;    }    protected void SetClipBuffer(ClipAction action, CoreRow[] rows)    {        ClipBuffer = new Tuple<ClipAction, CoreRow[]>(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);    }    /// <summary>    /// Reorder the given rows, to place them at <paramref name="index"/>; that is, the first row of <paramref name="rows"/> will    /// be at <paramref name="index"/> after this method executes. If <paramref name="isCopy"/> is <see langword="true"/>, the items will copied, rather    /// than moved.    /// </summary>    /// <remarks>    /// To move the rows to the end, <paramref name="index"/> should be equal to the number of rows in <see cref="Data"/>.    /// </remarks>    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    /// <summary>    /// Handle a number of rows from a different <see cref="DynamicGrid{T}"/> being dragged into this one.    /// </summary>    /// <param name="entity">The type of entity that that the rows of <paramref name="table"/> represent.</param>    /// <param name="table">The data being dragged.</param>    /// <param name="e"></param>    protected virtual void OnDragEnd(Type entity, CoreTable table, DragEventArgs e)    {        Logger.Send(LogType.Information,"","OnDragEnd");    }    /// <summary>    /// Handle all types of items being dragged onto this grid that aren't handled by <see cref="OnDragEnd(Type, CoreTable, DragEventArgs)"/>,    /// i.e., data which is not a <see cref="CoreTable"/> from another <see cref="DynamicGrid{T}"/>    /// </summary>    /// <remarks>    /// Can be used to handle files, for example.    /// </remarks>    /// <param name="sender"></param>    /// <param name="e"></param>    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}/// <summary>/// Shows that this <see cref="BaseDynamicGrid"/> can be used to import data./// </summary>public interface IImportDynamicGrid{    void DoImport();}/// <summary>/// Shows that this <see cref="BaseDynamicGrid"/> can be used to export data./// </summary>public interface IExportDynamicGrid{    void DoExport();}/// <summary>/// Shows that this <see cref="BaseDynamicGrid"/> can be used to duplicate data./// </summary>public interface IDuplicateDynamicGrid{    bool DoDuplicate(CoreRow[] rows);}/// <summary>/// Shows that this <see cref="BaseDynamicGrid"/> can show a help menu./// </summary>public interface IHelpDynamicGrid{    string HelpSlug();}
 |