DFLayout.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using Expressive;
  6. using InABox.Clients;
  7. namespace InABox.Core
  8. {
  9. public interface IDFRenderer
  10. {
  11. object? GetFieldValue(string field);
  12. /// <summary>
  13. /// Retrieve a piece of additional data for a field.
  14. /// </summary>
  15. /// <param name="fieldName">The field name, which is the variable code.</param>
  16. /// <param name="dataField">The specific field to be retrieved from the variable.</param>
  17. /// <returns>A value, which is specific to the type of <paramref name="fieldName"/> and the specific <paramref name="dataField"/> being
  18. /// retrieved.</returns>
  19. object? GetFieldData(string fieldName, string dataField);
  20. void SetFieldValue(string field, object? value);
  21. }
  22. public class DFLayout
  23. {
  24. public DFLayout()
  25. {
  26. ColumnWidths = new List<string>();
  27. RowHeights = new List<string>();
  28. Elements = new List<DFLayoutControl>();
  29. Expressions = new Dictionary<string, CoreExpression>();
  30. VariableReferences = new Dictionary<string, List<string>>();
  31. }
  32. public List<string> ColumnWidths { get; }
  33. public List<string> RowHeights { get; }
  34. public List<DFLayoutControl> Elements { get; }
  35. private Dictionary<string, CoreExpression> Expressions;
  36. private Dictionary<string, List<string>> VariableReferences;
  37. public IDFRenderer? Renderer;
  38. public string SaveLayout()
  39. {
  40. var sb = new StringBuilder();
  41. foreach (var column in ColumnWidths)
  42. sb.AppendFormat("C {0}\n", column);
  43. foreach (var row in RowHeights)
  44. sb.AppendFormat("R {0}\n", row);
  45. foreach (var element in Elements)
  46. sb.AppendFormat("E {0} {1}\n", element.GetType().EntityName(), element.SaveToString());
  47. var result = sb.ToString();
  48. return result;
  49. }
  50. private static Dictionary<string, Type>? _controls;
  51. /// <returns>A type which is a <see cref="DFLayoutControl"/></returns>
  52. private Type? GetElementType(string typeName)
  53. {
  54. _controls ??= CoreUtils.TypeList(
  55. AppDomain.CurrentDomain.GetAssemblies(),
  56. x => x.IsClass
  57. && !x.IsAbstract
  58. && !x.IsGenericType
  59. && typeof(DFLayoutControl).IsAssignableFrom(x)
  60. ).ToDictionary(
  61. x => x.EntityName(),
  62. x => x);
  63. return _controls.GetValueOrDefault(typeName);
  64. }
  65. public void LoadLayout(string layout)
  66. {
  67. ColumnWidths.Clear();
  68. RowHeights.Clear();
  69. Elements.Clear();
  70. var lines = layout.Split('\n');
  71. foreach (var line in lines)
  72. if (line.StartsWith("C "))
  73. {
  74. ColumnWidths.Add(line.Substring(2));
  75. }
  76. else if (line.StartsWith("R "))
  77. {
  78. RowHeights.Add(line.Substring(2));
  79. }
  80. else if (line.StartsWith("E ") || line.StartsWith("O "))
  81. {
  82. var typename = line.Split(' ').Skip(1).FirstOrDefault()
  83. ?.Replace("InABox.Core.Design", "InABox.Core.DFLayout")
  84. ?.Replace("DFLayoutChoiceField", "DFLayoutOptionField");
  85. if (!string.IsNullOrWhiteSpace(typename))
  86. {
  87. var type = GetElementType(typename);
  88. if(type != null)
  89. {
  90. var element = (Activator.CreateInstance(type) as DFLayoutControl)!;
  91. var json = string.Join(" ", line.Split(' ').Skip(2));
  92. element.LoadFromString(json);
  93. //Serialization.DeserializeInto(json, element);
  94. Elements.Add(element);
  95. }
  96. else
  97. {
  98. Logger.Send(LogType.Error, ClientFactory.UserID, $"{typename} is not the name of any concrete DFLayoutControls!");
  99. }
  100. }
  101. }
  102. //else if (line.StartsWith("O "))
  103. //{
  104. // String typename = line.Split(' ').Skip(1).FirstOrDefault()?.Replace("PRSDesktop", "InABox.Core");
  105. // if (!String.IsNullOrWhiteSpace(typename))
  106. // {
  107. // Type type = Type.GetType(typename);
  108. // DesignControl element = Activator.CreateInstance(type) as DesignControl;
  109. // if (element != null)
  110. // {
  111. // String json = String.Join(" ", line.Split(' ').Skip(2));
  112. // element.LoadFromString(json);
  113. // //CoreUtils.DeserializeInto(json, element);
  114. // }
  115. // Elements.Add(element);
  116. // }
  117. //}
  118. // Invalid Line Hmmm..
  119. if (!ColumnWidths.Any())
  120. ColumnWidths.AddRange(new[] { "*", "Auto" });
  121. if (!RowHeights.Any())
  122. RowHeights.AddRange(new[] { "Auto" });
  123. }
  124. private void AddVariableReference(string reference, string fieldName)
  125. {
  126. if (reference.Contains('.'))
  127. reference = reference.Split('.')[0];
  128. if(!VariableReferences.TryGetValue(reference, out var refs))
  129. {
  130. refs = new List<string>();
  131. VariableReferences[reference] = refs;
  132. }
  133. refs.Add(fieldName);
  134. }
  135. private object? GetFieldValue(string field)
  136. {
  137. if (field.Contains('.'))
  138. {
  139. var parts = field.Split('.');
  140. return Renderer?.GetFieldData(parts[0], string.Join('.', parts.Skip(1)));
  141. }
  142. else
  143. {
  144. return Renderer?.GetFieldValue(field);
  145. }
  146. }
  147. private void EvaluateExpression(string name)
  148. {
  149. var expression = Expressions[name];
  150. var values = new Dictionary<string, object?>();
  151. foreach (var field in expression.ReferencedVariables)
  152. {
  153. values[field] = GetFieldValue(field);
  154. }
  155. var oldValue = Renderer?.GetFieldValue(name);
  156. try
  157. {
  158. var value = expression?.Evaluate(values);
  159. if(value != oldValue)
  160. {
  161. Renderer?.SetFieldValue(name, value);
  162. }
  163. }
  164. catch (Exception e)
  165. {
  166. Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}");
  167. }
  168. }
  169. public void LoadVariable(DigitalFormVariable variable, DFLayoutField field)
  170. {
  171. var properties = variable.LoadProperties(field);
  172. if (!string.IsNullOrWhiteSpace(properties?.Expression))
  173. {
  174. var expression = new CoreExpression(properties.Expression);
  175. foreach (var reference in expression.ReferencedVariables)
  176. {
  177. AddVariableReference(reference, field.Name);
  178. }
  179. Expressions[field.Name] = expression;
  180. }
  181. }
  182. public void LoadVariables(IEnumerable<DigitalFormVariable> variables)
  183. {
  184. foreach (var field in Elements.Where(x => x is DFLayoutField).Cast<DFLayoutField>())
  185. {
  186. var variable = variables.FirstOrDefault(x => string.Equals(x.Code, field.Name));
  187. if (variable != null)
  188. {
  189. LoadVariable(variable, field);
  190. }
  191. }
  192. }
  193. public static DFLayout FromLayoutString(string layoutString)
  194. {
  195. var layout = new DFLayout();
  196. layout.LoadLayout(layoutString);
  197. return layout;
  198. }
  199. #region Expression Fields
  200. public void ChangeField(string fieldName)
  201. {
  202. if (!VariableReferences.TryGetValue(fieldName, out var refs)) return;
  203. foreach(var reference in refs)
  204. {
  205. EvaluateExpression(reference);
  206. }
  207. }
  208. public void EvaluateExpressions()
  209. {
  210. foreach(var name in Expressions.Keys)
  211. {
  212. EvaluateExpression(name);
  213. }
  214. }
  215. #endregion
  216. #region Auto-generated Layouts
  217. public static DFLayoutField? GenerateLayoutFieldFromVariable(DigitalFormVariable variable)
  218. {
  219. DFLayoutField? field = Activator.CreateInstance(variable.FieldType()) as DFLayoutField;
  220. if(field == null)
  221. {
  222. return null;
  223. }
  224. field.Name = variable.Code;
  225. return field;
  226. }
  227. public static DFLayout GenerateAutoDesktopLayout(
  228. IEnumerable<DigitalFormVariable> variables)
  229. {
  230. var layout = new DFLayout();
  231. layout.ColumnWidths.Add("*");
  232. layout.ColumnWidths.Add("Auto");
  233. layout.ColumnWidths.Add("Auto");
  234. for (var i = 0; i < variables.Count(); i++)
  235. {
  236. var variable = variables.ElementAt(i);
  237. layout.RowHeights.Add("Auto");
  238. var rowNum = new DFLayoutLabel { Caption = (i + 1).ToString(), Row = i + 1, Column = 1 };
  239. var label = new DFLayoutLabel { Caption = variable.Code, Row = i + 1, Column = 2 };
  240. layout.Elements.Add(rowNum);
  241. layout.Elements.Add(label);
  242. var field = GenerateLayoutFieldFromVariable(variable);
  243. if(field != null)
  244. {
  245. field.Row = i + 1;
  246. field.Column = 3;
  247. layout.Elements.Add(field);
  248. }
  249. }
  250. return layout;
  251. }
  252. public static DFLayout GenerateAutoMobileLayout(
  253. IEnumerable<DigitalFormVariable> variables)
  254. {
  255. var layout = new DFLayout();
  256. layout.ColumnWidths.Add("Auto");
  257. layout.ColumnWidths.Add("*");
  258. var row = 1;
  259. for (var i = 0; i < variables.Count(); i++)
  260. {
  261. var variable = variables.ElementAt(i);
  262. layout.RowHeights.Add("Auto");
  263. layout.RowHeights.Add("Auto");
  264. var rowNum = new DFLayoutLabel { Caption = i + 1 + ".", Row = row, Column = 1 };
  265. var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };
  266. layout.Elements.Add(rowNum);
  267. layout.Elements.Add(label);
  268. var field = GenerateLayoutFieldFromVariable(variable);
  269. if(field != null)
  270. {
  271. field.Row = row + 1;
  272. field.Column = 1;
  273. field.ColumnSpan = 2;
  274. layout.Elements.Add(field);
  275. }
  276. row += 2;
  277. }
  278. return layout;
  279. }
  280. public static DFLayout GenerateAutoLayout(DFLayoutType type, IEnumerable<DigitalFormVariable> variables)
  281. {
  282. return type switch
  283. {
  284. DFLayoutType.Mobile => GenerateAutoDesktopLayout(variables),
  285. _ => GenerateAutoDesktopLayout(variables),
  286. };
  287. }
  288. public static DFLayoutField? GenerateLayoutFieldFromEditor(BaseEditor editor)
  289. {
  290. // TODO: Finish
  291. switch (editor)
  292. {
  293. case CheckBoxEditor _:
  294. var newField = new DFLayoutBooleanField();
  295. newField.Properties.Type = DesignBooleanFieldType.Checkbox;
  296. return newField;
  297. case CheckListEditor _:
  298. // TODO: At this point, it seems CheckListEditor is unused.
  299. throw new NotImplementedException();
  300. case UniqueCodeEditor _:
  301. case CodeEditor _:
  302. return new DFLayoutCodeField();
  303. /* Not implemented because we don't like it.
  304. case PopupEditor v:*/
  305. case CodePopupEditor codePopupEditor:
  306. // TODO: Let's look at this later. For now, using a lookup.
  307. var newLookupFieldPopup = new DFLayoutLookupField();
  308. newLookupFieldPopup.Properties.LookupType = codePopupEditor.Type.EntityName();
  309. return newLookupFieldPopup;
  310. case ColorEditor _:
  311. return new DFLayoutColorField();
  312. case CurrencyEditor _:
  313. // TODO: Make this a specialised editor
  314. return new DFLayoutDoubleField();
  315. case DateEditor _:
  316. return new DFLayoutDateField();
  317. case DateTimeEditor _:
  318. return new DFLayoutDateTimeField();
  319. case DoubleEditor _:
  320. return new DFLayoutDoubleField();
  321. case DurationEditor _:
  322. return new DFLayoutTimeField();
  323. case EmbeddedImageEditor _:
  324. return new DFLayoutEmbeddedImage();
  325. case FileNameEditor _:
  326. case FolderEditor _:
  327. // Unimplemented because these editors only apply to properties for server engine configuration; it
  328. // doesn't make sense to store filenames in the database, and hence no entity will ever try to be saved
  329. // with a property with these editors.
  330. throw new NotImplementedException("This has intentionally been left unimplemented.");
  331. case IntegerEditor _:
  332. return new DFLayoutIntegerField();
  333. case ComboLookupEditor _:
  334. case EnumLookupEditor _:
  335. var newComboLookupField = new DFLayoutOptionField();
  336. var comboValuesTable = (editor as StaticLookupEditor)!.Values("Key");
  337. newComboLookupField.Properties.Options = string.Join(",", comboValuesTable.ExtractValues<string>("Key"));
  338. return newComboLookupField;
  339. case LookupEditor lookupEditor:
  340. var newLookupField = new DFLayoutLookupField();
  341. newLookupField.Properties.LookupType = lookupEditor.Type.EntityName();
  342. return newLookupField;
  343. case ImageDocumentEditor _:
  344. case MiscellaneousDocumentEditor _:
  345. case VectorDocumentEditor _:
  346. case PDFDocumentEditor _:
  347. var newDocField = new DFLayoutDocumentField();
  348. newDocField.Properties.FileMask = (editor as BaseDocumentEditor)!.FileMask;
  349. return newDocField;
  350. case NotesEditor _:
  351. return new DFLayoutNotesField();
  352. case NullEditor _:
  353. return null;
  354. case PasswordEditor _:
  355. return new DFLayoutPasswordField();
  356. case PINEditor _:
  357. var newPINField = new DFLayoutPINField();
  358. newPINField.Properties.Length = ClientFactory.PINLength;
  359. return newPINField;
  360. // TODO: Implement JSON editors and RichText editors.
  361. case JsonEditor _:
  362. case MemoEditor _:
  363. case RichTextEditor _:
  364. case ScriptEditor _:
  365. return new DFLayoutTextField();
  366. case TextBoxEditor _:
  367. return new DFLayoutStringField();
  368. case TimestampEditor _:
  369. return new DFLayoutTimeStampField();
  370. case TimeOfDayEditor _:
  371. return new DFLayoutTimeField();
  372. case URLEditor _:
  373. return new DFLayoutURLField();
  374. }
  375. return null;
  376. }
  377. public static DFLayout GenerateEntityLayout(Type entityType)
  378. {
  379. var layout = new DFLayout();
  380. layout.ColumnWidths.Add("Auto");
  381. layout.ColumnWidths.Add("*");
  382. var properties = DatabaseSchema.Properties(entityType);
  383. var Row = 1;
  384. foreach (var property in properties)
  385. {
  386. var editor = EditorUtils.GetPropertyEditor(entityType, property);
  387. if (editor != null && !(editor is NullEditor) && editor.Editable != Editable.Hidden)
  388. {
  389. var field = GenerateLayoutFieldFromEditor(editor);
  390. if (field != null)
  391. {
  392. var label = new DFLayoutLabel { Caption = editor.Caption };
  393. label.Row = Row;
  394. label.Column = 1;
  395. field.Row = Row;
  396. field.Column = 2;
  397. field.Name = property.Name;
  398. layout.Elements.Add(label);
  399. layout.Elements.Add(field);
  400. layout.RowHeights.Add("Auto");
  401. Row++;
  402. }
  403. }
  404. }
  405. return layout;
  406. }
  407. public static DFLayout GenerateEntityLayout<T>()
  408. {
  409. return GenerateEntityLayout(typeof(T));
  410. }
  411. #endregion
  412. }
  413. }