|  | @@ -323,6 +323,7 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          private readonly Label EditSpacer;
 | 
	
		
			
				|  |  |          private readonly Button Export;
 | 
	
		
			
				|  |  |          private readonly Label ExportSpacer;
 | 
	
		
			
				|  |  | +        private readonly Button DuplicateBtn;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          private readonly GridRowSizingOptions gridRowResizingOptions = new() { CanIncludeHiddenColumns = false, AutoFitMode = AutoFitMode.SmartFit };
 | 
	
		
			
				|  |  |          private readonly Button Help;
 | 
	
	
		
			
				|  | @@ -382,6 +383,11 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              MasterColumns = new DynamicGridColumns();
 | 
	
		
			
				|  |  |              MasterColumns.ExtractColumns(typeof(T), "");
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +            foreach(var column in LookupFactory.RequiredColumns<T>().ColumnNames())
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                AddHiddenColumn(column);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              ActionColumns = new DynamicActionColumns();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              if (IsSequenced)
 | 
	
	
		
			
				|  | @@ -548,6 +554,8 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              Delete.SetValue(DockPanel.DockProperty, Dock.Right);
 | 
	
		
			
				|  |  |              Delete.Click += Delete_Click;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +            DuplicateBtn = AddButton("Duplicate", Properties.Resources.paste.AsBitmapImage(Color.White), DoDuplicate);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              Count = new Label();
 | 
	
		
			
				|  |  |              Count.Height = 30;
 | 
	
		
			
				|  |  |              Count.Margin = new Thickness(0, 2, 0, 0);
 | 
	
	
		
			
				|  | @@ -662,6 +670,9 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |                  up.Position = Options.Contains(DynamicGridOption.EditRows) ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
 | 
	
		
			
				|  |  |              if (down != null)
 | 
	
		
			
				|  |  |                  down.Position = Options.Contains(DynamicGridOption.EditRows) ? DynamicActionColumnPosition.Start : DynamicActionColumnPosition.Hidden;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (DuplicateBtn != null)
 | 
	
		
			
				|  |  | +                DuplicateBtn.Visibility = Visibility.Collapsed;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          protected override DynamicGridStyleSelector<T> GetStyleSelector()
 | 
	
	
		
			
				|  | @@ -1028,6 +1039,9 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          protected virtual void SelectItems(CoreRow[]? rows)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              OnSelectItem?.Invoke(this, new DynamicGridSelectionEventArgs(rows));
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            DuplicateBtn.Visibility =
 | 
	
		
			
				|  |  | +                typeof(T).IsAssignableTo(typeof(IDuplicatable)) && rows != null && rows.Length >= 1 ? Visibility.Visible : Visibility.Collapsed;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          private bool bFilterVisible;
 | 
	
	
		
			
				|  | @@ -2092,6 +2106,8 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          #region Item Manipulation
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        #region Load/Save/Delete
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          protected virtual T[] LoadItems(CoreRow[] rows)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              var result = new List<T>();
 | 
	
	
		
			
				|  | @@ -2143,6 +2159,10 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              DoDelete();
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        #endregion
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        #region Edit
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          protected virtual void DoEdit()
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              if (!SelectedRows.Any())
 | 
	
	
		
			
				|  | @@ -2270,11 +2290,6 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              return false;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        protected virtual void ShowHelp(string slug)
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + slug) { UseShellExecute = true });
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          protected virtual void DoAdd()
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              //CoreRow row = (SelectedRow > -1) && (SelectedRow < Data.Rows.Count) ?  Data.Rows[this.SelectedRow] : null;
 | 
	
	
		
			
				|  | @@ -2289,138 +2304,6 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              DoAdd();
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        protected void ReloadForms<TTargetType, TTargetForm, TSourceForm>(DynamicEditorForm editor, TTargetType item,
 | 
	
		
			
				|  |  | -            Expression<Func<TSourceForm, object?>> sourcekey, Guid sourceid)
 | 
	
		
			
				|  |  | -            where TTargetType : Entity, new()
 | 
	
		
			
				|  |  | -            where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new()
 | 
	
		
			
				|  |  | -            where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm<TTargetType>, new()
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm));
 | 
	
		
			
				|  |  | -            var page =
 | 
	
		
			
				|  |  | -                editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid<TTargetType, TTargetForm>;
 | 
	
		
			
				|  |  | -            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<TSourceForm>().Query(
 | 
	
		
			
				|  |  | -                        new Filter<TSourceForm>(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo)
 | 
	
		
			
				|  |  | -                            .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last())
 | 
	
		
			
				|  |  | -                    );
 | 
	
		
			
				|  |  | -                }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                var newforms = new List<TTargetForm>();
 | 
	
		
			
				|  |  | -                foreach (var row in table.Rows)
 | 
	
		
			
				|  |  | -                {
 | 
	
		
			
				|  |  | -                    var sourceform = row.ToObject<TSourceForm>();
 | 
	
		
			
				|  |  | -                    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<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;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -            if (!IsSequenced)
 | 
	
		
			
				|  |  | -                return;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -            using (new WaitCursor())
 | 
	
		
			
				|  |  | -            {
 | 
	
		
			
				|  |  | -                var updates = ClipBuffer.Item2.Select(x => x.ToObject<T>()).ToList();
 | 
	
		
			
				|  |  | -                if (BeforePaste(updates, ClipBuffer.Item1))
 | 
	
		
			
				|  |  | -                {
 | 
	
		
			
				|  |  | -                    var currow = SelectedRows.FirstOrDefault()
 | 
	
		
			
				|  |  | -                        ?? Data.Rows.LastOrDefault();
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                    var sequence = currow != null ? currow.Get<T, long>(c => ((ISequenceable)c).Sequence) : 0;
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -                    var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get<ISequenceable, long>(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<T> items, ClipAction action)
 | 
	
		
			
				|  |  | -        {
 | 
	
		
			
				|  |  | -            return true;
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        #endregion
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -        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();
 | 
	
		
			
				|  |  | -        }
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          public virtual DynamicEditorPages LoadEditorPages(T item)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              DynamicEditorPages pages = new DynamicEditorPages();
 | 
	
	
		
			
				|  | @@ -2442,7 +2325,7 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |                  (f, i) =>
 | 
	
		
			
				|  |  |                  {
 | 
	
		
			
				|  |  |                      Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + typeof(T).Name.SplitCamelCase().Replace(" ", "_"))
 | 
	
		
			
				|  |  | -                        { UseShellExecute = true });
 | 
	
		
			
				|  |  | +                    { UseShellExecute = true });
 | 
	
		
			
				|  |  |                  }
 | 
	
		
			
				|  |  |              );
 | 
	
		
			
				|  |  |          }
 | 
	
	
		
			
				|  | @@ -2456,14 +2339,14 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              var cursor = new WaitCursor();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              var pages = items.Length == 1 ? LoadEditorPages(items.First()) : new DynamicEditorPages();
 | 
	
		
			
				|  |  | -            
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              var buttons = new DynamicEditorButtons();
 | 
	
		
			
				|  |  |              if (items.Length == 1)
 | 
	
		
			
				|  |  |                  LoadEditorButtons(items.First(), buttons);
 | 
	
		
			
				|  |  | -            
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              var editor = new DynamicEditorForm(items.Any() ? items.First().GetType() : typeof(T), pages, buttons, PageDataHandler, PreloadPages);
 | 
	
		
			
				|  |  |              editor.SetValue(Panel.ZIndexProperty, 999);
 | 
	
		
			
				|  |  | -            
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              editor.OnCustomiseColumns += (o, c) =>
 | 
	
		
			
				|  |  |              {
 | 
	
		
			
				|  |  |                  ConfigureColumns(MasterColumns /*, true */);
 | 
	
	
		
			
				|  | @@ -2533,7 +2416,7 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |              return editor.ShowDialog() == true;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          private void UpdateEditor(DynamicEditorGrid grid, Expression<Func<IDimensioned, object?>> property, bool enabled)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              if (!grid.TryFindEditor(new Column<IDimensioned>(property).Property, out var editor))
 | 
	
	
		
			
				|  | @@ -2552,15 +2435,14 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              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);
 | 
	
		
			
				|  |  | +                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 string[]? ValidateData(T[] items)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              var errors = new List<string>();
 | 
	
	
		
			
				|  | @@ -2572,7 +2454,7 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          protected virtual void DoValidate(T[] items, List<string> errors)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | -        
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          protected virtual void AfterLoad(DynamicEditorForm editor, T[] items)
 | 
	
	
		
			
				|  | @@ -2592,7 +2474,7 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |                  foreach (var key in newchanges.Keys)
 | 
	
		
			
				|  |  |                      result[key] = newchanges[key];
 | 
	
		
			
				|  |  |              }
 | 
	
		
			
				|  |  | -            
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |              return result;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -2687,7 +2569,6 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -
 | 
	
		
			
				|  |  |          private bool AddEditClick(CoreRow[]? rows)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              if (!IsEnabled || bRefreshing)
 | 
	
	
		
			
				|  | @@ -2747,11 +2628,184 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              return false;
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        #endregion
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        #region Duplicate
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        protected virtual IEnumerable<T> 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<T>(x => x.ID).InList(ids));
 | 
	
		
			
				|  |  | +            return true;
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        #endregion
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        protected virtual void ShowHelp(string slug)
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            Process.Start(new ProcessStartInfo("https://prs-software.com.au/wiki/index.php/" + slug) { UseShellExecute = true });
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        protected void ReloadForms<TTargetType, TTargetForm, TSourceForm>(DynamicEditorForm editor, TTargetType item,
 | 
	
		
			
				|  |  | +            Expression<Func<TSourceForm, object?>> sourcekey, Guid sourceid)
 | 
	
		
			
				|  |  | +            where TTargetType : Entity, new()
 | 
	
		
			
				|  |  | +            where TTargetForm : Entity, IRemotable, IPersistent, IDigitalFormInstance, new()
 | 
	
		
			
				|  |  | +            where TSourceForm : Entity, IRemotable, IPersistent, IDigitalForm<TTargetType>, new()
 | 
	
		
			
				|  |  | +        {
 | 
	
		
			
				|  |  | +            var type = typeof(IDynamicOneToManyGrid<,>).MakeGenericType(typeof(TTargetType), typeof(TTargetForm));
 | 
	
		
			
				|  |  | +            var page =
 | 
	
		
			
				|  |  | +                editor.Pages?.FirstOrDefault(x => x.GetType().GetInterfaces().Contains(type)) as IDynamicOneToManyGrid<TTargetType, TTargetForm>;
 | 
	
		
			
				|  |  | +            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<TSourceForm>().Query(
 | 
	
		
			
				|  |  | +                        new Filter<TSourceForm>(sourcekey).IsEqualTo(sourceid).And(x => x.Form.AppliesTo)
 | 
	
		
			
				|  |  | +                            .IsEqualTo(typeof(TTargetType).EntityName().Split('.').Last())
 | 
	
		
			
				|  |  | +                    );
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                var newforms = new List<TTargetForm>();
 | 
	
		
			
				|  |  | +                foreach (var row in table.Rows)
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var sourceform = row.ToObject<TSourceForm>();
 | 
	
		
			
				|  |  | +                    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<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;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            if (!IsSequenced)
 | 
	
		
			
				|  |  | +                return;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +            using (new WaitCursor())
 | 
	
		
			
				|  |  | +            {
 | 
	
		
			
				|  |  | +                var updates = ClipBuffer.Item2.Select(x => x.ToObject<T>()).ToList();
 | 
	
		
			
				|  |  | +                if (BeforePaste(updates, ClipBuffer.Item1))
 | 
	
		
			
				|  |  | +                {
 | 
	
		
			
				|  |  | +                    var currow = SelectedRows.FirstOrDefault()
 | 
	
		
			
				|  |  | +                        ?? Data.Rows.LastOrDefault();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    var sequence = currow != null ? currow.Get<T, long>(c => ((ISequenceable)c).Sequence) : 0;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +                    var postrows = Data.Rows.Where(r => !ClipBuffer.Item2.Contains(r) && r.Get<ISequenceable, long>(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<T> 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)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              Data.LoadRow(row, obj);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        #region Import / Export
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          protected virtual Guid GetImportID()
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              return Guid.Empty;
 | 
	
	
		
			
				|  | @@ -2859,6 +2913,8 @@ namespace InABox.DynamicGrid
 | 
	
		
			
				|  |  |              ExcelExporter.DoExport(data, filename);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        #endregion
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          public void ScrollIntoView(CoreRow row)
 | 
	
		
			
				|  |  |          {
 | 
	
		
			
				|  |  |              DataGrid.ScrollInView(new RowColumnIndex(row.Index + 1, 0));
 |