DynamicVariableGrid.cs 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  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.Clients;
  10. using InABox.Core;
  11. using InABox.Wpf;
  12. using InABox.WPF;
  13. namespace InABox.DynamicGrid;
  14. public class DynamicVariableGrid : DynamicDataGrid<DigitalFormVariable>
  15. {
  16. private bool ShowHidden = false;
  17. private Button ShowHiddenButton;
  18. private Button HideButton;
  19. public DigitalForm? Form { get; set; }
  20. private DigitalFormVariable[]? _variables;
  21. public DigitalFormVariable[] Variables
  22. {
  23. get
  24. {
  25. _variables ??= Data.ToArray<DigitalFormVariable>();
  26. return _variables;
  27. }
  28. }
  29. public DynamicFormLayoutGrid? LayoutGrid { get; set; }
  30. protected override void Init()
  31. {
  32. base.Init();
  33. ShowHiddenButton = AddButton("Show Hidden", null, ToggleHidden_Click);
  34. HideButton = AddEditButton("Hide Variable", null, Hide_Click);
  35. HideButton.IsEnabled = false;
  36. HiddenColumns.Add(x => x.Hidden);
  37. HiddenColumns.Add(x => x.Code);
  38. HiddenColumns.Add(x => x.VariableType);
  39. HiddenColumns.Add(x => x.Parameters);
  40. HiddenColumns.Add(x => x.Required);
  41. HiddenColumns.Add(x => x.Secure);
  42. HiddenColumns.Add(x => x.Retain);
  43. ActionColumns.Add(new DynamicMenuColumn(BuildMenu) { Position = DynamicActionColumnPosition.End });
  44. }
  45. protected override void DoReconfigure(DynamicGridOptions options)
  46. {
  47. base.DoReconfigure(options);
  48. options.MultiSelect = true;
  49. }
  50. private void BuildMenu(DynamicMenuColumn column, CoreRow? row)
  51. {
  52. if (row is null) return;
  53. if (Security.CanEdit<DigitalFormVariable>())
  54. {
  55. var variable = row.ToObject<DigitalFormVariable>();
  56. var changeType = column.AddItem("Change Type", null, null);
  57. foreach(var (toType, converter) in DFUtils.GetFieldConverters(variable.FieldType()))
  58. {
  59. var caption = toType.GetCaption();
  60. if (string.IsNullOrWhiteSpace(caption))
  61. {
  62. caption = CoreUtils.Neatify(toType.Name);
  63. }
  64. changeType.AddItem(caption, null, (variable, converter, toType), ChangeType_Click);
  65. }
  66. if(changeType.Items.Count == 0)
  67. {
  68. changeType.IsEnabled = false;
  69. }
  70. }
  71. }
  72. private void ChangeType_Click((DigitalFormVariable variable, Type converterType, Type fieldType) tuple)
  73. {
  74. if (Form is null) return;
  75. var (variable, converterType, fieldType) = tuple;
  76. var oldFieldType = variable.FieldType();
  77. var converter = (Activator.CreateInstance(converterType) as IDFLayoutFieldConverter)!;
  78. var oldProperties = variable.CreateProperties();
  79. variable.SetFieldType(fieldType);
  80. var newPropertiesType = DigitalFormVariable.GetPropertiesType(fieldType);
  81. if (newPropertiesType is null) return;
  82. var newProperties = (Activator.CreateInstance(newPropertiesType) as DFLayoutFieldProperties)!;
  83. foreach(var property in DatabaseSchema.LocalProperties(oldProperties.GetType()))
  84. {
  85. if(DatabaseSchema.Property(newProperties.GetType(), property.Name) is IProperty prop)
  86. {
  87. prop.Setter()(newProperties, property.Getter()(oldProperties));
  88. }
  89. }
  90. converter.ConvertProperties(oldProperties, newProperties);
  91. newProperties.SetDefaultValue(converter.Convert(oldProperties, oldProperties.GetDefaultValue(), newProperties));
  92. if (DynamicVariableUtils.EditProperties(Form, Variables, newProperties.GetType(), newProperties, false))
  93. {
  94. if(!MessageWindow.ShowYesNo(
  95. $"This will convert field '{variable.Code}' from {oldFieldType.GetCaption()} to {fieldType.GetCaption()}. Note that this will " +
  96. $"update all instances of this form '{Form.Code}' that have been filled in, converting the field data. Do you wish to proceed?",
  97. "Proceed?"))
  98. {
  99. return;
  100. }
  101. variable.SaveProperties(fieldType, newProperties);
  102. if (DFUtils.GetFormInstanceType(Form.AppliesTo) is not Type instanceType)
  103. {
  104. throw new Exception($"Could not find form instance that applies to {Form.AppliesTo}");
  105. }
  106. var forms = Client.Create(instanceType)
  107. .Query(
  108. Filter.Create(instanceType).All(),
  109. Columns.Create(instanceType, ColumnTypeFlags.Required)
  110. .Add<IDigitalFormInstance>(x => x.FormData)
  111. .Add<IDigitalFormInstance>(x => x.BlobData))
  112. .ToObjects(instanceType)
  113. .Cast<Entity>()
  114. .ToArray();
  115. foreach(var form in forms)
  116. {
  117. var formInstance = (form as IDigitalFormInstance)!;
  118. var newData = DigitalForm.DeserializeFormSaveData(formInstance) ?? new();
  119. var oldData = newData.ToLoadStorage();
  120. var entry = oldData.GetEntry(variable.Code);
  121. var oldValue = oldProperties.Deserialize(oldData.GetEntry(variable.Code));
  122. var newValue = converter.Convert(oldProperties, oldValue, newProperties);
  123. newProperties.Serialize(newData.GetEntry(variable.Code), newValue);
  124. DigitalForm.SerializeFormData(formInstance, newData);
  125. }
  126. Client.Create(instanceType).Save(forms, $"Changed {variable.Code} from {oldFieldType.GetCaption()} to {fieldType.GetCaption()}");
  127. if(LayoutGrid is not null)
  128. {
  129. foreach(var layout in LayoutGrid.Layouts)
  130. {
  131. var dfLayout = DFLayout.FromLayoutString(layout.Layout);
  132. void UpdateList(List<DFLayoutControl> elements)
  133. {
  134. var updates = new List<(int, DFLayoutField)>();
  135. foreach(var (i, element) in elements.WithIndex())
  136. {
  137. if (element is not DFLayoutField field
  138. || field.Name != variable.Code) continue;
  139. updates.Add((i, field));
  140. }
  141. foreach(var (i, field) in updates)
  142. {
  143. var newField = (Activator.CreateInstance(fieldType) as DFLayoutField)!;
  144. newField.LoadFromString(field.SaveToString());
  145. elements[i] = newField;
  146. }
  147. }
  148. UpdateList(dfLayout.Elements);
  149. UpdateList(dfLayout.HiddenElements);
  150. layout.Layout = dfLayout.SaveLayout();
  151. }
  152. LayoutGrid.SaveItems(LayoutGrid.Layouts);
  153. LayoutGrid.Refresh(false, true);
  154. }
  155. SaveItem(variable);
  156. Refresh(false, true);
  157. }
  158. }
  159. public DigitalFormVariable? GetVariable(string code)
  160. {
  161. return Data.Rows
  162. .Where(x => x.Get<DigitalFormVariable, string>(x => x.Code) == code)
  163. .FirstOrDefault()
  164. ?.ToObject<DigitalFormVariable>();
  165. }
  166. private static bool ShouldHide(CoreRow[] rows)
  167. {
  168. return rows.Any(x => !x.Get<DigitalFormVariable, bool>(x => x.Hidden));
  169. }
  170. private bool Hide_Click(Button btn, CoreRow[] rows)
  171. {
  172. if(rows.Length == 0)
  173. {
  174. MessageBox.Show("No rows selected");
  175. return false;
  176. }
  177. var hide = ShouldHide(rows);
  178. var items = rows.ToArray<DigitalFormVariable>();
  179. foreach (var item in items)
  180. {
  181. item.Hidden = hide;
  182. }
  183. if(items.Length > 0)
  184. {
  185. SaveItems(items);
  186. return true;
  187. }
  188. return false;
  189. }
  190. protected override void SelectItems(CoreRow[]? rows)
  191. {
  192. base.SelectItems(rows);
  193. if(rows is null)
  194. {
  195. HideButton.IsEnabled = false;
  196. }
  197. else
  198. {
  199. HideButton.IsEnabled = true;
  200. HideButton.Content = (ShouldHide(rows) ? "Hide Variable" : "Un-hide Variable") + (rows.Length > 1 ? "s" : "");
  201. }
  202. }
  203. private bool ToggleHidden_Click(Button btn, CoreRow[] rows)
  204. {
  205. ShowHidden = !ShowHidden;
  206. ShowHiddenButton.Content = ShowHidden ? "Hide Hidden" : "Show Hidden";
  207. return true;
  208. }
  209. private void CreateMenu(ContextMenu parent, string header, Type type)
  210. {
  211. if (Form is null) return;
  212. parent.AddItem(header, null, type, (itemtype) =>
  213. {
  214. if(DynamicVariableUtils.CreateAndEdit(Form, Data.ToArray<DigitalFormVariable>(), itemtype, out var variable))
  215. {
  216. SaveItem(variable);
  217. Refresh(false, true);
  218. }
  219. });
  220. }
  221. protected override void DoAdd(bool openEditorOnDirectEdit = false)
  222. {
  223. var menu = new ContextMenu();
  224. foreach(var fieldType in DFUtils.GetFieldTypes())
  225. {
  226. var caption = fieldType.GetCaption();
  227. if (string.IsNullOrWhiteSpace(caption))
  228. {
  229. caption = CoreUtils.Neatify(fieldType.Name);
  230. }
  231. CreateMenu(menu, caption, fieldType);
  232. }
  233. menu.IsOpen = true;
  234. }
  235. protected override bool CanCreateItems()
  236. {
  237. return base.CanCreateItems() && Form is not null;
  238. }
  239. public override DigitalFormVariable CreateItem()
  240. {
  241. var item = base.CreateItem();
  242. item.Form.ID = Form?.ID ?? Guid.Empty;
  243. return item;
  244. }
  245. protected override void DoEdit()
  246. {
  247. if (!SelectedRows.Any() || Form is null)
  248. return;
  249. var variable = SelectedRows.First().ToObject<DigitalFormVariable>();
  250. var properties = variable.CreateProperties();
  251. if (DynamicVariableUtils.EditProperties(Form, Variables, properties.GetType(), properties, !Security.CanEdit<DigitalFormVariable>()))
  252. {
  253. variable.SaveProperties(properties);
  254. SaveItem(variable);
  255. Refresh(false, true);
  256. }
  257. }
  258. protected override void DoDelete()
  259. {
  260. var rows = SelectedRows.ToArray();
  261. if (rows.Length != 0)
  262. if (CanDeleteItems(rows))
  263. if (MessageWindow.ShowYesNo(
  264. "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.)",
  265. "Confirm Deletion"))
  266. {
  267. DeleteItems(rows);
  268. SelectedRows = Array.Empty<CoreRow>();
  269. DoChanged();
  270. Refresh(false, true);
  271. SelectItems(null);
  272. }
  273. }
  274. protected override bool FilterRecord(CoreRow row)
  275. {
  276. return ShowHidden || !row.Get<DigitalFormVariable, bool>(x => x.Hidden);
  277. }
  278. protected override void OnAfterRefresh()
  279. {
  280. base.OnAfterRefresh();
  281. _variables = null;
  282. }
  283. protected override void Reload(Filters<DigitalFormVariable> criteria, Columns<DigitalFormVariable> columns, ref SortOrder<DigitalFormVariable>? sort, CancellationToken token, Action<CoreTable?, Exception?> action)
  284. {
  285. if(Form is null)
  286. {
  287. criteria.Add(Filter.None<DigitalFormVariable>());
  288. }
  289. else
  290. {
  291. criteria.Add(Filter<DigitalFormVariable>.Where(x => x.Form.ID).IsEqualTo(Form.ID));
  292. }
  293. base.Reload(criteria, columns, ref sort, token, action);
  294. }
  295. }
  296. public class DFLookupFilterNode : ComboBox, ICustomValueNode
  297. {
  298. public DFLookupFilterNode(Type entityType, CustomFilterValue? selectedValue)
  299. {
  300. var properties = CoreUtils.PropertyList(entityType, x => x.GetCustomAttribute<DoNotSerialize>() == null, true).Keys.ToList();
  301. properties.Sort();
  302. Items.Add("");
  303. foreach (var property in properties)
  304. {
  305. Items.Add(property);
  306. }
  307. Value = selectedValue;
  308. SelectionChanged += DFLookupFilterNode_SelectionChanged;
  309. VerticalAlignment = System.Windows.VerticalAlignment.Stretch;
  310. VerticalContentAlignment = System.Windows.VerticalAlignment.Center;
  311. MinWidth = 50;
  312. }
  313. private void DFLookupFilterNode_SelectionChanged(object sender, SelectionChangedEventArgs e)
  314. {
  315. var text = SelectedItem as string;
  316. if (text.IsNullOrWhiteSpace())
  317. {
  318. _value = null;
  319. }
  320. else
  321. {
  322. _value = new CustomFilterValue(System.Text.Encoding.UTF8.GetBytes(text));
  323. }
  324. ValueChanged?.Invoke(this, Value);
  325. }
  326. private CustomFilterValue? _value;
  327. public CustomFilterValue? Value
  328. {
  329. get => _value;
  330. set
  331. {
  332. if(value is not null)
  333. {
  334. var text = System.Text.Encoding.UTF8.GetString(value.Data);
  335. SelectedItem = text;
  336. _value = value;
  337. }
  338. else
  339. {
  340. SelectedItem = null;
  341. _value = null;
  342. }
  343. }
  344. }
  345. public FrameworkElement FrameworkElement => this;
  346. public event ICustomValueNode.ValueChangedHandler? ValueChanged;
  347. }
  348. public static class DynamicVariableUtils
  349. {
  350. public static bool CreateAndEdit(
  351. DigitalForm form, IList<DigitalFormVariable> variables,
  352. Type fieldType,
  353. [NotNullWhen(true)] out DigitalFormVariable? variable)
  354. {
  355. var fieldBaseType = fieldType.GetSuperclassDefinition(typeof(DFLayoutField<>));
  356. if (fieldBaseType != null)
  357. {
  358. var propertiesType = fieldBaseType.GetGenericArguments()[0];
  359. var properties = (Activator.CreateInstance(propertiesType) as DFLayoutFieldProperties)!;
  360. if (DynamicVariableUtils.EditProperties(form, variables, propertiesType, properties, readOnly: false))
  361. {
  362. variable = new DigitalFormVariable();
  363. variable.Form.CopyFrom(form);
  364. variable.SaveProperties(fieldType, properties);
  365. return true;
  366. }
  367. }
  368. variable = null;
  369. return false;
  370. }
  371. public static bool EditProperties(DigitalForm form, IList<DigitalFormVariable> variables, Type type, DFLayoutFieldProperties item, bool readOnly)
  372. {
  373. var editor = new DynamicEditorForm(type);
  374. editor.ReadOnly = readOnly;
  375. if (item is DFLayoutLookupFieldProperties)
  376. {
  377. var appliesToType = DFUtils.FormEntityType(form);
  378. editor.OnReconfigureEditors = grid =>
  379. {
  380. var filter = grid.FindEditor("Filter");
  381. if(filter is FilterEditorControl filterEditor)
  382. {
  383. var config = new FilterEditorConfiguration();
  384. config.OnCreateCustomValueNode += (type, prop, op, value) =>
  385. {
  386. if (appliesToType is not null)
  387. {
  388. return new DFLookupFilterNode(appliesToType, value);
  389. }
  390. else
  391. {
  392. return null;
  393. }
  394. };
  395. filterEditor.Configuration = config;
  396. }
  397. };
  398. editor.OnFormCustomiseEditor += (sender, items, column, editor) => LookupEditor_OnFormCustomiseEditor(sender, variables, items, column, editor);
  399. editor.OnEditorValueChanged += (sender, name, value) =>
  400. {
  401. var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
  402. if (name == "LookupType")
  403. {
  404. var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
  405. var edit = grid.FindEditor("Filter");
  406. if (edit is FilterEditorControl filter)
  407. {
  408. filter.FilterType = value is string str ?
  409. CoreUtils.GetEntityOrNull(str) :
  410. null;
  411. }
  412. var propertiesEditor = grid.FindEditor(nameof(DFLayoutLookupFieldProperties.AdditionalProperties));
  413. if (propertiesEditor is MultiLookupEditorControl multi && multi.EditorDefinition is MultiLookupEditor combo)
  414. {
  415. combo.Clear();
  416. multi.Configure();
  417. }
  418. }
  419. OnEditorValueChanged(sender, name, value);
  420. return new();
  421. };
  422. }
  423. else
  424. {
  425. editor.OnFormCustomiseEditor += (sender, items, column, editor) => Editor_OnFormCustomiseEditor(sender, variables, column, editor);
  426. editor.OnEditorValueChanged += (sender, name, value) =>
  427. {
  428. var result = DynamicGridUtils.UpdateEditorValue(new[] { item }, name, value);
  429. OnEditorValueChanged(sender, name, value);
  430. return result;
  431. };
  432. }
  433. editor.OnCreateEditorControl += Editor_OnCreateEditorControl;
  434. editor.OnDefineLookups += o =>
  435. {
  436. var def = (o.EditorDefinition as ILookupEditor)!;
  437. var colname = o.ColumnName;
  438. // Nope, there is nothing dodgy about this at all
  439. // I am not breaking any rules by passing in the QA Form instance, rather than the Field instance
  440. // so that I can get access to the "AppliesTo" property, and thus the list of properties that can be updated
  441. // Nothing to see here, I promise!
  442. CoreTable? values;
  443. if (o.ColumnName == "Property")
  444. {
  445. values = def.Values(colname, new[] { form });
  446. }
  447. else
  448. {
  449. values = def.Values(colname, new[] { item });
  450. }
  451. o.LoadLookups(values);
  452. };
  453. var thisVariable = variables.Where(x => x.Code == item.Code).ToList();
  454. editor.OnValidateData += (sender, items) =>
  455. {
  456. var errors = new List<string>();
  457. foreach(var item in items.Cast<DFLayoutFieldProperties>())
  458. {
  459. // Check Codes
  460. if (string.IsNullOrWhiteSpace(item.Code))
  461. {
  462. errors.Add("[Code] may not be blank!");
  463. }
  464. else
  465. {
  466. var codeVars = variables.Where(x => x.Code == item.Code).ToList();
  467. if(codeVars.Count > 1)
  468. {
  469. errors.Add($"Duplicate code [{item.Code}]");
  470. }
  471. else if(codeVars.Count == 1)
  472. {
  473. if (!thisVariable.Contains(codeVars.First()))
  474. {
  475. errors.Add($"There is already a variable with code [{item.Code}]");
  476. }
  477. }
  478. }
  479. // Check Read-Only property
  480. if(item.ReadOnlyProperty && item.Property.IsNullOrWhiteSpace() && item.Expression.IsNullOrWhiteSpace())
  481. {
  482. errors.Add("A field cannot be read-only if [Property] or [Expression] have not been set.");
  483. }
  484. }
  485. return errors;
  486. };
  487. editor.Items = new BaseObject[] { item };
  488. return editor.ShowDialog() == true;
  489. }
  490. private static void Editor_OnCreateEditorControl(string column, BaseEditor editor, IDynamicEditorControl control)
  491. {
  492. var properties = (control.Host.GetItems()[0] as DFLayoutFieldProperties)!;
  493. if(column == nameof(DFLayoutFieldProperties.ReadOnlyProperty))
  494. {
  495. if (properties.Property.IsNullOrWhiteSpace())
  496. {
  497. control.IsEnabled = false;
  498. }
  499. }
  500. }
  501. private static void Editor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, DynamicGridColumn column, BaseEditor editor)
  502. {
  503. var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
  504. if ((column.ColumnName == "Expression" || column.ColumnName == "ColourExpression") && editor is ExpressionEditor exp)
  505. {
  506. var variables = new List<string>();
  507. foreach (var variable in vars)
  508. {
  509. //variables.Add(variable.Code);
  510. foreach (var col in variable.GetVariableColumns())
  511. {
  512. variables.Add(col.ColumnName);
  513. }
  514. }
  515. if(column.ColumnName == "Expression")
  516. {
  517. variables.Remove(properties.Code);
  518. }
  519. variables.Sort();
  520. exp.VariableNames = variables;
  521. }
  522. }
  523. private static void LookupEditor_OnFormCustomiseEditor(IDynamicEditorForm sender, IList<DigitalFormVariable> vars, object[] items, DynamicGridColumn column, BaseEditor editor)
  524. {
  525. if (column.ColumnName == "Filter" && editor is FilterEditor fe)
  526. {
  527. var properties = (items[0] as DFLayoutLookupFieldProperties)!;
  528. var lookupType = properties.LookupType;
  529. var entityType = CoreUtils.GetEntityOrNull(lookupType);
  530. fe.Type = entityType;
  531. }
  532. Editor_OnFormCustomiseEditor(sender, vars, column, editor);
  533. }
  534. private static void OnEditorValueChanged(IDynamicEditorForm sender, string name, object value)
  535. {
  536. if(name == nameof(DFLayoutFieldProperties.Property))
  537. {
  538. var properties = (sender.Items[0] as DFLayoutFieldProperties)!;
  539. var grid = (sender as EmbeddedDynamicEditorForm)?.Editor!;
  540. var edit = grid.FindEditor(nameof(DFLayoutFieldProperties.ReadOnlyProperty));
  541. if (edit is not null)
  542. {
  543. edit.IsEnabled = !properties.Property.IsNullOrWhiteSpace() && !grid.ReadOnly && edit.EditorDefinition.Editable.IsEditable();
  544. }
  545. }
  546. }
  547. }