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; using System.Threading; using System.Diagnostics.CodeAnalysis; namespace InABox.DynamicGrid; public abstract class DynamicGrid : BaseDynamicGrid, IDynamicGridUIComponentParent, IDynamicGrid, IImportDynamicGrid, IExportDynamicGrid, IDuplicateDynamicGrid, IHelpDynamicGrid where T : BaseObject, new() { #region Events public ValidateEvent? OnValidate { get; set; } public event OnDefineFilter? OnDefineFilter; public event OnCreateItem? OnCreateItem; public event OnAfterCreateItem? OnAfterCreateItem; public event EditorValueChangedHandler? OnEditorValueChanged; public event OnCustomiseEditor? OnCustomiseEditor; public event EntitySaveEvent? OnBeforeSave; public event EntitySaveEvent? OnAfterSave; public delegate void EditorLoaded(IDynamicEditorForm editor, T[] items); public event EditorLoaded? OnEditorLoaded; public event OnLoadEditorButtons? OnLoadEditorButtons; #endregion protected virtual string HelpSlug() { return typeof(T).Name.Split('.').Last().SplitCamelCase().Replace(" ", "_"); } string IHelpDynamicGrid.HelpSlug() => HelpSlug(); protected override bool CanDuplicate => typeof(T).IsAssignableTo(typeof(IDuplicatable)); public DynamicGrid() : base() { } protected sealed override void PreInit() { MasterColumns = new DynamicGridColumns(); MasterColumns.ExtractColumns(typeof(T)); HiddenColumns = new HiddenColumnsList(); if (typeof(T).IsAssignableTo(typeof(ISequenceable))) { HiddenColumns.Add(x => (x as ISequenceable)!.Sequence); } } protected override void Init() { } #region IDynamicGridUIComponentParent protected override IDynamicGridUIComponent CreateUIComponent() { return new DynamicGridGridUIComponent { Parent = this }; } T IDynamicGrid.LoadItem(CoreRow row) => LoadItem(row); 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); } #endregion protected override DynamicGridRowStyleSelector GetRowStyleSelector() { return new DynamicGridRowStyleSelector(); } public DynamicGridColumns MasterColumns { get; protected 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 set; } private static bool IsSequenced => typeof(T).GetInterfaces().Any(x => x.Equals(typeof(ISequenceable))); #region Options protected override void DoReconfigure(DynamicGridOptions options) { options.ReorderRows = IsSequenced; } #endregion 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; } GetUIComponent().UpdateRow(row); } #region Column Handling /// /// Create a set of according to the natural default sizes and formats and captions, based on the contents of . /// /// /// protected DynamicGridColumns ExtractColumns(Columns columns) { var cols = new DynamicGridColumns(); foreach (var col in columns) { var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property)); if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden) cols.Add(mc); } return cols; } DynamicGridColumns IDynamicGrid.ExtractColumns(IColumns columns) { var cols = new DynamicGridColumns(); foreach (var col in columns) { var mc = MasterColumns.FirstOrDefault(x => x.ColumnName.Equals(col.Property)); if (mc != null && mc.Editor is not NullEditor && mc.Editor.Visible != Visible.Hidden) cols.Add(mc); } return cols; } public override DynamicGridColumns GenerateColumns() { var cols = IsDirectEditMode() ? new Columns(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeForeignKeys) : new Columns(ColumnTypeFlags.IncludeVisible | ColumnTypeFlags.IncludeLinked); var columns = ExtractColumns(cols); OnGenerateColumns?.Invoke(this, new GenerateColumnsEventArgs(columns)); return columns; } public event GenerateColumnsEvent? OnGenerateColumns; public event SaveColumnsEvent? OnSaveColumns; public event GetAvailableColumnsEvent? GetAvailableColumns; protected override void SaveColumns(DynamicGridColumns columns) { OnSaveColumns?.Invoke(this, new(columns)); } #endregion #region Refresh / Reload protected abstract void Reload( Filters criteria, Columns columns, ref SortOrder? sort, CancellationToken token, Action action); public Filter? DefineFilter() { if (OnDefineFilter is null) return null; var result = OnDefineFilter.Invoke(typeof(T)) as Filter; return result; } 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; } protected override void ReloadData(CancellationToken token, Action action) { _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, token, action); } 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)); if (!Options.ReadOnly) { foreach (var column in LookupFactory.RequiredColumns()) { columns.Add(column); } } return columns; } public void UpdateRow(CoreRow row, T obj, bool invalidateRow = true) { ObjectToRow(obj, row); ObjectToRow(obj, _recordmap[row]); if (invalidateRow) { InvalidateRow(row); } } public void UpdateRows(CoreRow[] rows, T[] objs, bool invalidateRows = true) { for(var i = 0; i < objs.Length; ++i) { UpdateRow(rows[i], objs[i], invalidateRow: invalidateRows); } } public void AddRow(T data) { if (MasterData is null) return; MasterData.LoadRow(data); Refresh(false, false); } #endregion #region Item Manipulation #region Load/Save/Delete public virtual T[] LoadItems(IList rows) { return rows.ToArray(LoadItem); } public abstract T LoadItem(CoreRow row); public abstract void SaveItem(T item); public virtual void SaveItems(IEnumerable 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 override bool CanDeleteRows(params CoreRow[] rows) => CanDeleteItems(rows); public override void DeleteRows(params CoreRow[] rows) => DeleteItems(rows); #endregion #region Edit protected override void NewRow() { CreateItems(null); } 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(); DoChanged(); } 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); pages.RemoveAll(x => !x.Visible); 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); }; 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(sender, (T[])items, column, editor); OnCustomiseEditor?.Invoke(sender, (T[])items, column, editor); } protected virtual void CustomiseEditor(IDynamicEditorForm form, 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); } /// /// Edit the with a non-modal editor window, attaching them to the provided. /// /// List of objects to edit. /// A callback for when the items are finished being edited. public virtual void EditItemsNonModal(ISubPanelHost host, T[] items, Action callback, Func? PageDataHandler = null, bool PreloadPages = false) { if (!DynamicGridUtils.TryEdit(items, out var editLock)) { if(editLock.Panel is Window window) { Task.Delay(100).ContinueWith(task => { window.WindowState = WindowState.Normal; window.Activate(); }, TaskScheduler.FromCurrentSynchronizationContext()); } else { MessageWindow.ShowMessage("One or more items are already being edited in an open window. You cannot edit the same entity multiple times simultaneously.", "Simultaneous edit."); } return; } DynamicEditorForm editor; using (var cursor = new WaitCursor()) { editor = new DynamicEditorForm(); editor.SetValue(Panel.ZIndexProperty, 999); editor.Form.DisableOKIfUnchanged = true; InitialiseEditorForm(editor, items, PageDataHandler, PreloadPages); OnEditorLoaded?.Invoke(editor, items); } editLock.Panel = editor; host.AddSubPanel(editor); editor.SubPanelClosed += o => { try { DynamicGridUtils.FinishEdit(items); callback(items, editor.Result == true); } catch(Exception e) { MessageWindow.ShowError("Error occurred while closing editor.", e); } }; editor.Show(); } /// /// Edit the with a modal editor window. /// /// List of objects to edit. /// if the items were saved. public virtual bool EditItems(T[] items, Func? PageDataHandler = null, bool PreloadPages = false) { DynamicEditorForm editor; using (var cursor = new WaitCursor()) { editor = new DynamicEditorForm() { WindowStartupLocation = WindowStartupLocation.CenterScreen }; 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(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(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 ?? DatabaseSchema.Property(item.GetType(), column.ColumnName)?.Editor; } 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 override bool CanCreateRows() => CanCreateItems(); protected virtual bool CanCreateItems() { return true; } protected override bool EditRows(CoreRow[]? rows) { if(rows is null) { if (!CanCreateItems()) return false; var item = CreateItem(); if (!AfterCreate(item)) return false; if(Options.NonModalEditorHost is not null) { EditItemsNonModal(Options.NonModalEditorHost, [item], (items, result) => { if (result) { var row = Data.NewRow(); ObjectToRow(item, row); Data.Rows.Add(row); var masterrow = MasterData.NewRow(); ObjectToRow(item, masterrow); MasterData.Rows.Add(masterrow); _recordmap[row] = masterrow; InvalidateGrid(); SelectedRows = [row]; DoChanged(); } }); return false; } else { if (EditItems([item])) { //_CurrentRow = Data.Rows.Count; var row = Data.NewRow(); ObjectToRow(item, row); Data.Rows.Add(row); var masterrow = MasterData.NewRow(); ObjectToRow(item, masterrow); MasterData.Rows.Add(masterrow); _recordmap[row] = masterrow; InvalidateGrid(); SelectedRows = [row]; DoChanged(); return true; } return false; } } else { 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 snaps = items.ToArray(x => x.TakeSnapshot()); if(Options.NonModalEditorHost is not null) { EditItemsNonModal(Options.NonModalEditorHost, items, (items, result) => { if (result) { var sel = SelectedRows; UpdateRows(rows, items, invalidateRows: false); InvalidateGrid(); SelectedRows = sel; DoChanged(); } else { foreach(var snap in snaps) { snap.ResetObject(); } } }); return false; } else { if (EditItems(items)) { var sel = SelectedRows; UpdateRows(rows, items, invalidateRows: false); InvalidateGrid(); SelectedRows = sel; DoChanged(); return true; } else { foreach(var snap in snaps) { snap.ResetObject(); } } } return false; } return false; } } #endregion #region Duplicate protected virtual IEnumerable LoadDuplicatorItems(CoreRow[] rows) { return LoadItems(rows); } bool IDuplicateDynamicGrid.DoDuplicate(CoreRow[] rows) => DoDuplicate(rows); protected virtual bool DoDuplicate(CoreRow[] rows) { if (rows.Length == 0) { 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 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 protected virtual bool BeforeCopy(IList items) { return true; } protected override void MoveRows(CoreRow[] rows, int index, bool isCopy = false) { if (!Options.ReorderRows || !IsSequenced) return; var items = LoadItems(rows); if (isCopy) { if (!BeforeCopy(items)) { return; } } var sequence = index < Data.Rows.Count ? Data.Rows[index].Get(x => x.Sequence) : Data.Rows[^1].Get(x => x.Sequence) + 1; var postRows = Data.Rows.Where(r => !rows.Contains(r) && r.Get(x => x.Sequence) >= sequence); var updates = items.Concatenate(LoadItems(postRows.ToArray())); foreach (var update in updates) { ((ISequenceable)update).Sequence = sequence; sequence++; } if (updates.Length != 0) { SaveItems(updates); DoChanged(); Refresh(false, true); } } #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; } void IImportDynamicGrid.DoImport() => DoImport(); 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); } 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) { } void IExportDynamicGrid.DoExport() => DoExport(); 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 = GetUIComponent().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 LookupFactory.RequiredColumns()) { columns.Add(column); } foreach (var (column, _) in predicates) { reloadColumns.Add(column); } CoreTable? data = null; void LoadExport() { var newData = new CoreTable(); newData.LoadColumns(columns); FilterRows(data.Rows, 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); } var sort = LookupFactory.DefineSort(); Reload(filters, reloadColumns, ref sort, CancellationToken.None, (table, err) => { if (table is not null) { if (table.Offset == 0 || data is null) { data = table; if (!IsPaging) { Dispatcher.Invoke(LoadExport); } } else { data.AddPage(table); if (!IsPaging) { Dispatcher.Invoke(LoadExport); } } } else if (err is not null) { Dispatcher.Invoke(() => { MessageWindow.ShowError("Error in export.", err); }); } }); } /// /// 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 #endregion #region Header Actions protected override bool SelectColumns([NotNullWhen(true)] out DynamicGridColumns? columns) { var schema = new DynamicGridObjectColumnSchema(typeof(T)); var editor = new DynamicGridColumnsEditor(schema, typeof(T)) { WindowStartupLocation = WindowStartupLocation.CenterScreen }; editor.DirectEdit = IsDirectEditMode(); editor.Columns.AddRange(VisibleColumns); schema.OnProcessColumns += args => { GetAvailableColumns?.Invoke(args); }; if (editor.ShowDialog().Equals(true)) { columns = editor.Columns; return true; } else { columns = null; return false; } } #endregion #region Drag + Drop 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 override 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; } // 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); [MemberNotNull(nameof(entity))] set => entity = value.EntityName(); } public DynamicGridDragFormat(DataTable table, Type entity) { Table = table; Entity = entity; } }