DynamicVariableGrid.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics.CodeAnalysis;
  4. using System.Linq;
  5. using System.Reflection;
  6. using System.Threading;
  7. using System.Windows;
  8. using System.Windows.Controls;
  9. using InABox.Core;
  10. using InABox.Wpf;
  11. using InABox.WPF;
  12. using MathNet.Numerics;
  13. using NPOI.OpenXmlFormats;
  14. using NPOI.SS.Formula.Functions;
  15. using NPOI.Util.Collections;
  16. namespace InABox.DynamicGrid;
  17. public class DynamicVariableGrid : DynamicDataGrid<DigitalFormVariable>
  18. {
  19. private bool ShowHidden = false;
  20. private Button ShowHiddenButton;
  21. private Button HideButton;
  22. public DigitalForm? Form { get; set; }
  23. private DigitalFormVariable[]? _variables;
  24. public DigitalFormVariable[] Variables
  25. {
  26. get
  27. {
  28. _variables ??= Data.ToArray<DigitalFormVariable>();
  29. return _variables;
  30. }
  31. }
  32. protected override void Init()
  33. {
  34. base.Init();
  35. ShowHiddenButton = AddButton("Show Hidden", null, ToggleHidden_Click);
  36. HideButton = AddEditButton("Hide Variable", null, Hide_Click);
  37. HideButton.IsEnabled = false;
  38. HiddenColumns.Add(x => x.Hidden);
  39. HiddenColumns.Add(x => x.Code);
  40. HiddenColumns.Add(x => x.VariableType);
  41. HiddenColumns.Add(x => x.Parameters);
  42. HiddenColumns.Add(x => x.Required);
  43. HiddenColumns.Add(x => x.Secure);
  44. HiddenColumns.Add(x => x.Retain);
  45. }
  46. protected override void DoReconfigure(DynamicGridOptions options)
  47. {
  48. base.DoReconfigure(options);
  49. options.MultiSelect = true;
  50. }
  51. public DigitalFormVariable? GetVariable(string code)
  52. {
  53. return Data.Rows
  54. .Where(x => x.Get<DigitalFormVariable, string>(x => x.Code) == code)
  55. .FirstOrDefault()
  56. ?.ToObject<DigitalFormVariable>();
  57. }
  58. private static bool ShouldHide(CoreRow[] rows)
  59. {
  60. return rows.Any(x => !x.Get<DigitalFormVariable, bool>(x => x.Hidden));
  61. }
  62. private bool Hide_Click(Button btn, CoreRow[] rows)
  63. {
  64. if(rows.Length == 0)
  65. {
  66. MessageBox.Show("No rows selected");
  67. return false;
  68. }
  69. var hide = ShouldHide(rows);
  70. var items = rows.ToArray<DigitalFormVariable>();
  71. foreach (var item in items)
  72. {
  73. item.Hidden = hide;
  74. }
  75. if(items.Length > 0)
  76. {
  77. SaveItems(items);
  78. return true;
  79. }
  80. return false;
  81. }
  82. protected override void SelectItems(CoreRow[]? rows)
  83. {
  84. base.SelectItems(rows);
  85. if(rows is null)
  86. {
  87. HideButton.IsEnabled = false;
  88. }
  89. else
  90. {
  91. HideButton.IsEnabled = true;
  92. HideButton.Content = (ShouldHide(rows) ? "Hide Variable" : "Un-hide Variable") + (rows.Length > 1 ? "s" : "");
  93. }
  94. }
  95. private bool ToggleHidden_Click(Button btn, CoreRow[] rows)
  96. {
  97. ShowHidden = !ShowHidden;
  98. ShowHiddenButton.Content = ShowHidden ? "Hide Hidden" : "Show Hidden";
  99. return true;
  100. }
  101. private void CreateMenu(ContextMenu parent, string header, Type type)
  102. {
  103. if (Form is null) return;
  104. parent.AddItem(header, null, type, (itemtype) =>
  105. {
  106. if(DynamicVariableUtils.CreateAndEdit(Form, Data.ToArray<DigitalFormVariable>(), itemtype, out var variable))
  107. {
  108. SaveItem(variable);
  109. Refresh(false, true);
  110. }
  111. });
  112. }
  113. protected override void DoAdd(bool openEditorOnDirectEdit = false)
  114. {
  115. var menu = new ContextMenu();
  116. foreach(var fieldType in DFUtils.GetFieldTypes())
  117. {
  118. var caption = fieldType.GetCaption();
  119. if (string.IsNullOrWhiteSpace(caption))
  120. {
  121. caption = CoreUtils.Neatify(fieldType.Name);
  122. }
  123. CreateMenu(menu, caption, fieldType);
  124. }
  125. menu.IsOpen = true;
  126. }
  127. protected override bool CanCreateItems()
  128. {
  129. return base.CanCreateItems() && Form is not null;
  130. }
  131. public override DigitalFormVariable CreateItem()
  132. {
  133. var item = base.CreateItem();
  134. item.Form.ID = Form?.ID ?? Guid.Empty;
  135. return item;
  136. }
  137. protected override void DoEdit()
  138. {
  139. if (!SelectedRows.Any() || Form is null)
  140. return;
  141. var variable = SelectedRows.First().ToObject<DigitalFormVariable>();
  142. var properties = variable.CreateProperties();
  143. if (DynamicVariableUtils.EditProperties(Form, Variables, properties.GetType(), properties, !Security.CanEdit<DigitalFormVariable>()))
  144. {
  145. variable.SaveProperties(properties);
  146. SaveItem(variable);
  147. Refresh(false, true);
  148. }
  149. }
  150. protected override void DoDelete()
  151. {
  152. var rows = SelectedRows.ToArray();
  153. if (rows.Length != 0)
  154. if (CanDeleteItems(rows))
  155. if (MessageWindow.ShowYesNo(
  156. "Are you sure you want to delete this variable? This will all cause data associated with this variable to be lost.\n(If you want to just hide the variable, set it to 'Hidden' instead.)",
  157. "Confirm Deletion"))
  158. {
  159. DeleteItems(rows);
  160. SelectedRows = Array.Empty<CoreRow>();
  161. DoChanged();
  162. Refresh(false, true);
  163. SelectItems(null);
  164. }
  165. }
  166. protected override bool FilterRecord(CoreRow row)
  167. {
  168. return ShowHidden || !row.Get<DigitalFormVariable, bool>(x => x.Hidden);
  169. }
  170. protected override void OnAfterRefresh()
  171. {
  172. base.OnAfterRefresh();
  173. _variables = null;
  174. }
  175. protected override void Reload(Filters<DigitalFormVariable> criteria, Columns<DigitalFormVariable> columns, ref SortOrder<DigitalFormVariable>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
  176. {
  177. if(Form is null)
  178. {
  179. criteria.Add(Filter.None<DigitalFormVariable>());
  180. }
  181. else
  182. {
  183. criteria.Add(Filter<DigitalFormVariable>.Where(x => x.Form.ID).IsEqualTo(Form.ID));
  184. }
  185. base.Reload(criteria, columns, ref sort, token, action);
  186. }
  187. }
  188. public class DFLookupFilterNode : ComboBox, ICustomValueNode
  189. {
  190. public DFLookupFilterNode(Type entityType, CustomFilterValue? selectedValue)
  191. {
  192. var properties = CoreUtils.PropertyList(entityType, x => x.GetCustomAttribute<DoNotSerialize>() == null, true).Keys.ToList();
  193. properties.Sort();
  194. Items.Add("");
  195. foreach (var property in properties)
  196. {
  197. Items.Add(property);
  198. }
  199. Value = selectedValue;
  200. SelectionChanged += DFLookupFilterNode_SelectionChanged;
  201. VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
  202. VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
  203. MinWidth = 50;
  204. }
  205. private void DFLookupFilterNode_SelectionChanged(object sender, SelectionChangedEventArgs e)
  206. {
  207. var text = SelectedItem as string;
  208. if (text.IsNullOrWhiteSpace())
  209. {
  210. _value = null;
  211. }
  212. else
  213. {
  214. _value = new CustomFilterValue(System.Text.Encoding.UTF8.GetBytes(text));
  215. }
  216. ValueChanged?.Invoke(this, Value);
  217. }
  218. private CustomFilterValue? _value;
  219. public CustomFilterValue? Value
  220. {
  221. get => _value;
  222. set
  223. {
  224. if(value is not null)
  225. {
  226. var text = System.Text.Encoding.UTF8.GetString(value.Data);
  227. SelectedItem = text;
  228. _value = value;
  229. }
  230. else
  231. {
  232. SelectedItem = null;
  233. _value = null;
  234. }
  235. }
  236. }
  237. public FrameworkElement FrameworkElement => this;
  238. public event ICustomValueNode.ValueChangedHandler? ValueChanged;
  239. }
  240. public static class DynamicVariableUtils
  241. {
  242. public static bool CreateAndEdit(
  243. DigitalForm form, IList<DigitalFormVariable> variables,
  244. Type fieldType,
  245. [NotNullWhen(true)] out DigitalFormVariable? variable)
  246. {
  247. var fieldBaseType = fieldType.GetSuperclassDefinition(typeof(DFLayoutField<>));
  248. if (fieldBaseType != null)
  249. {
  250. var propertiesType = fieldBaseType.GetGenericArguments()[0];
  251. var properties = (Activator.CreateInstance(propertiesType) as DFLayoutFieldProperties)!;
  252. if (DynamicVariableUtils.EditProperties(form, variables, propertiesType, properties, readOnly: false))
  253. {
  254. variable = new DigitalFormVariable();
  255. variable.Form.CopyFrom(form);
  256. variable.SaveProperties(fieldType, properties);
  257. return true;
  258. }
  259. }
  260. variable = null;
  261. return false;
  262. }
  263. public static bool EditProperties(DigitalForm form, IList<DigitalFormVariable> variables, Type type, DFLayoutFieldProperties item, bool readOnly)
  264. {
  265. var editor = new DynamicEditorForm(type);
  266. editor.ReadOnly = readOnly;
  267. if (item is DFLayoutLookupFieldProperties)
  268. {
  269. var appliesToType = DFUtils.FormEntityType(form);
  270. editor.OnReconfigureEditors = grid =>
  271. {
  272. var filter = grid.FindEditor("Filter");
  273. if(filter is FilterEditorControl filterEditor)
  274. {
  275. var config = new FilterEditorConfiguration();
  276. config.OnCreateCustomValueNode += (type, prop, op, value) =>
  277. {
  278. if (appliesToType is not null)
  279. {
  280. return new DFLookupFilterNode(appliesToType, value);
  281. }
  282. else
  283. {
  284. return null;
  285. }
  286. };
  287. filterEditor.Configuration = config;
  288. }
  289. };
  290. editor.OnFormCustomiseEditor += (sender, items, column, editor) => LookupEditor_OnFormCustomiseEditor(sender, variables, items, column, editor);
  291. editor.OnEditorValueChanged += (sender, name, value) =>
  292. {
  293. var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
  294. if (name == "LookupType")
  295. {
  296. var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
  297. var edit = grid.FindEditor("Filter");
  298. if (edit is FilterEditorControl filter)
  299. {
  300. filter.FilterType = value is string str ?
  301. CoreUtils.GetEntityOrNull(str) :
  302. null;
  303. }
  304. var propertiesEditor = grid.FindEditor(nameof(DFLayoutLookupFieldProperties.AdditionalProperties));
  305. if (propertiesEditor is MultiLookupEditorControl multi && multi.EditorDefinition is MultiLookupEditor combo)
  306. {
  307. combo.Clear();
  308. multi.Configure();
  309. }
  310. }
  311. OnEditorValueChanged(sender, name, value);
  312. return new();
  313. };
  314. }
  315. else
  316. {
  317. editor.OnFormCustomiseEditor += (sender, items, column, editor) => Editor_OnFormCustomiseEditor(sender, variables, column, editor);
  318. editor.OnEditorValueChanged += (sender, name, value) =>
  319. {
  320. var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
  321. OnEditorValueChanged(sender, name, value);
  322. return result;
  323. };
  324. }
  325. editor.OnCreateEditorControl += Editor_OnCreateEditorControl;
  326. editor.OnDefineLookups += o =>
  327. {
  328. var def = (o.EditorDefinition as ILookupEditor)!;
  329. var colname = o.ColumnName;
  330. // Nope, there is nothing dodgy about this at all
  331. // I am not breaking any rules by passing in the QA Form instance, rather than the Field instance
  332. // so that I can get access to the "AppliesTo" property, and thus the list of properties that can be updated
  333. // Nothing to see here, I promise!
  334. CoreTable? values;
  335. if (o.ColumnName == "Property")
  336. {
  337. values = def.Values(colname, new[] { form });
  338. }
  339. else
  340. {
  341. values = def.Values(colname, new[] { item });
  342. }
  343. o.LoadLookups(values);
  344. };
  345. var thisVariable = variables.Where(x => x.Code == item.Code).ToList();
  346. editor.OnValidateData += (sender, items) =>
  347. {
  348. var errors = new List<string>();
  349. foreach(var item in items.Cast<DFLayoutFieldProperties>())
  350. {
  351. // Check Codes
  352. if (string.IsNullOrWhiteSpace(item.Code))
  353. {
  354. errors.Add("[Code] may not be blank!");
  355. }
  356. else
  357. {
  358. var codeVars = variables.Where(x => x.Code == item.Code).ToList();
  359. if(codeVars.Count > 1)
  360. {
  361. errors.Add($"Duplicate code [{item.Code}]");
  362. }
  363. else if(codeVars.Count == 1)
  364. {
  365. if (!thisVariable.Contains(codeVars.First()))
  366. {
  367. errors.Add($"There is already a variable with code [{item.Code}]");
  368. }
  369. }
  370. }
  371. // Check Read-Only property
  372. if(item.ReadOnlyProperty && item.Property.IsNullOrWhiteSpace() && item.Expression.IsNullOrWhiteSpace())
  373. {
  374. errors.Add("A field cannot be read-only if [Property] or [Expression] have not been set.");
  375. }
  376. }
  377. return errors;
  378. };
  379. editor.Items = new BaseObject[] { item };
  380. return editor.ShowDialog() == true;
  381. }
  382. private static void Editor_OnCreateEditorControl(string column, BaseEditor editor, IDynamicEditorControl control)
  383. {
  384. var properties = (control.Host.GetItems()[0] as DFLayoutFieldProperties)!;
  385. if(column == nameof(DFLayoutFieldProperties.ReadOnlyProperty))
  386. {
  387. if (properties.Property.IsNullOrWhiteSpace())
  388. {
  389. control.IsEnabled = false;
  390. }
  391. }
  392. }
  393. private static void Editor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, DynamicGridColumn column, BaseEditor editor)
  394. {
  395. var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
  396. if ((column.ColumnName == "Expression" || column.ColumnName == "ColourExpression") && editor is ExpressionEditor exp)
  397. {
  398. var variables = new List<string>();
  399. foreach (var variable in vars)
  400. {
  401. //variables.Add(variable.Code);
  402. foreach (var col in variable.GetVariableColumns())
  403. {
  404. variables.Add(col.ColumnName);
  405. }
  406. }
  407. if(column.ColumnName == "Expression")
  408. {
  409. variables.Remove(properties.Code);
  410. }
  411. variables.Sort();
  412. exp.VariableNames = variables;
  413. }
  414. }
  415. private static void LookupEditor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, object[] items, DynamicGridColumn column, BaseEditor editor)
  416. {
  417. if (column.ColumnName == "Filter" && editor is FilterEditor fe)
  418. {
  419. var properties = (items[0] as DFLayoutLookupFieldProperties)!;
  420. var lookupType = properties.LookupType;
  421. var entityType = CoreUtils.GetEntityOrNull(lookupType);
  422. fe.Type = entityType;
  423. }
  424. Editor_OnFormCustomiseEditor(sender, vars, column, editor);
  425. }
  426. private static void OnEditorValueChanged(IDynamicEditorForm sender, string name, object value)
  427. {
  428. if(name == nameof(DFLayoutFieldProperties.Property))
  429. {
  430. var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
  431. var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
  432. var edit = grid.FindEditor(nameof(DFLayoutFieldProperties.ReadOnlyProperty));
  433. if (edit is not null)
  434. {
  435. edit.IsEnabled = !properties.Property.IsNullOrWhiteSpace() && !grid.ReadOnly && edit.EditorDefinition.Editable.IsEditable();
  436. }
  437. }
  438. }
  439. }