CoreRepository.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.ComponentModel;
  5. using System.IO;
  6. using System.Linq;
  7. using System.Linq.Expressions;
  8. using System.Runtime.CompilerServices;
  9. using System.Threading.Tasks;
  10. using InABox.Clients;
  11. using InABox.Configuration;
  12. using InABox.Core;
  13. using InABox.Mobile.Shared;
  14. using JetBrains.Annotations;
  15. using Syncfusion.SfDataGrid.XForms;
  16. using Xamarin.CommunityToolkit.ObjectModel;
  17. using Xamarin.Forms;
  18. using Columns = InABox.Core.Columns;
  19. namespace InABox.Mobile
  20. {
  21. public class CoreRepositoryItemCreatedArgs<TShell> : EventArgs
  22. {
  23. public TShell Item { get; private set; }
  24. public CoreRepositoryItemCreatedArgs(TShell item)
  25. {
  26. Item = item;
  27. }
  28. }
  29. public delegate void CoreRepositoryItemCreatedEvent<TShell>(object sender, CoreRepositoryItemCreatedArgs<TShell> args);
  30. public abstract class CoreRepository
  31. {
  32. public static string CacheFileName<T>() => typeof(T).EntityName().Split('.').Last();
  33. public static string CacheFileName<T>(string postfix)
  34. {
  35. var result = CacheFileName<T>();
  36. if (!string.IsNullOrWhiteSpace(postfix))
  37. result = $"{result}_{postfix}";
  38. return result;
  39. }
  40. [CanBeNull] public abstract string FileName();
  41. public bool IsCached() => CoreRepository.IsCached(FileName());
  42. public static bool IsCached(string filename) =>
  43. !String.IsNullOrWhiteSpace(filename)
  44. && File.Exists(CacheFileName(filename));
  45. public static string CacheFileName(string filename) =>
  46. Path.Combine(CacheFolder(), $"{filename}.cache");
  47. public static string CacheFolder()
  48. {
  49. var result = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
  50. if (CacheID != Guid.Empty)
  51. result = Path.Combine(result,CacheID.ToString());
  52. if (!Directory.Exists(result))
  53. Directory.CreateDirectory(result);
  54. return result;
  55. }
  56. public static Guid CacheID { get; set; }
  57. }
  58. public abstract class CoreRepository<TParent, TItem, TEntity> : CoreRepository, ICoreRepository, IEnumerable<TItem>
  59. where TParent : CoreRepository<TParent, TItem, TEntity>
  60. where TEntity : Entity, IRemotable, IPersistent, new()
  61. where TItem : Shell<TParent,TEntity>, new()
  62. {
  63. readonly MultiQuery _query = new();
  64. public Func<Filter<TEntity>> Filter { get; set; }
  65. protected virtual Filter<TEntity> BaseFilter() => null;
  66. public IModelHost Host { get; set; }
  67. public DateTime LastUpdated { get; protected set; }
  68. [CanBeNull] private Func<string> _cacheFileName { get; set; }
  69. protected CoreRepository(IModelHost host, [CanBeNull] Func<Filter<TEntity>> filter = null, [CanBeNull] Func<string> cachefilename = null)
  70. {
  71. AllItems = new ObservableRangeCollection<TItem>();
  72. AllItems.CollectionChanged += (sender, args) => ItemsChanged(AllItems);
  73. EnableSynchronization(AllItems);
  74. Items = new ObservableRangeCollection<TItem>();
  75. EnableSynchronization(Items);
  76. SelectedItems = new ObservableRangeCollection<TItem>();
  77. EnableSynchronization(SelectedItems);
  78. Reset();
  79. Host = host;
  80. Filter = filter;
  81. _cacheFileName = cachefilename;
  82. }
  83. [CanBeNull] public override string FileName() => _cacheFileName?.Invoke();
  84. protected virtual void ItemsChanged(IEnumerable<TItem> items)
  85. {
  86. }
  87. private void EnableSynchronization(IEnumerable items)
  88. {
  89. BindingBase.EnableCollectionSynchronization(items, null,
  90. (collection, context, method, access) =>
  91. {
  92. lock (collection)
  93. {
  94. method?.Invoke();
  95. }
  96. }
  97. );
  98. }
  99. #region INotifyPropertyChanged
  100. public event PropertyChangedEventHandler PropertyChanged;
  101. protected void DoPropertyChanged(object sender, PropertyChangedEventArgs args)
  102. {
  103. PropertyChanged?.Invoke(sender, args);
  104. }
  105. protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
  106. {
  107. if (EqualityComparer<T>.Default.Equals(field, value))
  108. return false;
  109. field = value;
  110. OnPropertyChanged(propertyName);
  111. return true;
  112. }
  113. protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
  114. => DoPropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  115. #endregion
  116. #region Image Lookups
  117. public Dictionary<Guid, byte[]> Images { get; private set; } = new Dictionary<Guid, byte[]>();
  118. public ImageSource GetImageSource(Guid id)
  119. {
  120. ImageSource result = null;
  121. if (Images.TryGetValue(id, out byte[] data))
  122. result = ImageSource.FromStream(() => new MemoryStream(data));
  123. return result;
  124. }
  125. public byte[] GetImage(Guid id) => Images.GetValueOrDefault(id);
  126. public bool HasImages() => Images.Any();
  127. #endregion
  128. protected virtual string FilterTag => typeof(TEntity).EntityName().Split('.').Last();
  129. public CoreFilterDefinitions AvailableFilters()
  130. {
  131. var result = new CoreFilterDefinitions();
  132. if (!string.IsNullOrWhiteSpace(FilterTag))
  133. {
  134. var mobile = new GlobalConfiguration<CoreFilterDefinitions>(FilterTag)
  135. .Load()
  136. .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile);
  137. result.AddRange(mobile);
  138. }
  139. return result;
  140. }
  141. protected Filter<TEntity> SelectedFilter;
  142. public void SelectFilter(String name)
  143. {
  144. var definition = AvailableFilters().FirstOrDefault(x => String.Equals(x.Name, name));
  145. SelectedFilter = definition?.AsFilter<TEntity>();
  146. }
  147. protected Filter<TEntity> EffectiveFilter()
  148. {
  149. var filters = new Filters<TEntity>();
  150. filters.Add(BaseFilter());
  151. filters.Add(Filter?.Invoke());
  152. filters.Add(SelectedFilter);
  153. var result = filters.Combine();
  154. return result;
  155. }
  156. protected Columns<TOtherEntity> GetColumns<TOtherItem, TOtherEntity>()
  157. where TOtherItem : Shell<TParent, TOtherEntity>, new()
  158. where TOtherEntity : Entity, IRemotable, IPersistent, new()
  159. {
  160. return new TOtherItem().Columns.Columns;
  161. }
  162. protected virtual void Initialize()
  163. {
  164. Loaded = false;
  165. AllItems.Clear();
  166. Items.Clear();
  167. SelectedItems.Clear();
  168. Images.Clear();
  169. }
  170. public bool Loaded { get; protected set; }
  171. private void DoRefresh(bool force)
  172. {
  173. var curselected = SelectedItems.ToArray();
  174. Items.Clear();
  175. SelectedItems.Clear();
  176. if (!Loaded && IsCached(FileName()))
  177. {
  178. DoBeforeLoad();
  179. if (LoadFromStorage())
  180. {
  181. DoAfterLoad();
  182. SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x)));
  183. return;
  184. }
  185. }
  186. if ((force || !Loaded) && Host.IsConnected())
  187. {
  188. DoLoad();
  189. SaveToStorage();
  190. SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x)));
  191. return;
  192. }
  193. SelectedItems.AddRange(Items.Where(x=>curselected.Contains(x)));
  194. }
  195. private void AfterRefresh()
  196. {
  197. Loaded = true;
  198. Search();
  199. NotifyChanged();
  200. }
  201. public virtual ICoreRepository Refresh(bool force)
  202. {
  203. DoRefresh(force);
  204. AfterRefresh();
  205. return this;
  206. }
  207. public void Refresh(bool force, Action loaded)
  208. {
  209. Task.Run(() => { DoRefresh(force); })
  210. .BeginInvokeOnMainThread((_) =>
  211. {
  212. AfterRefresh();
  213. loaded?.Invoke();
  214. });
  215. }
  216. public void Reset()
  217. {
  218. Initialize();
  219. }
  220. public event CoreRepositoryChangedEvent Changed;
  221. protected void NotifyChanged() => Changed?.Invoke(this, new CoreRepositoryChangedEventArgs());
  222. public virtual SortOrder<TEntity> Sort => LookupFactory.DefineSort<TEntity>();
  223. protected ObservableRangeCollection<TItem> AllItems { get; private set; }
  224. private CoreTable _table = new CoreTable();
  225. public ObservableRangeCollection<TItem> Items { get; private set; }
  226. IEnumerable ICoreRepository.Items => Items;
  227. #region Item Selection
  228. public ObservableRangeCollection<TItem> SelectedItems { get; private set; }
  229. IEnumerable ICoreRepository.SelectedItems => SelectedItems;
  230. public bool IsSelected(TItem item) => (item != null) && SelectedItems.Contains(item);
  231. public void SetSelectedItems(IEnumerable<TItem> items)
  232. {
  233. SelectedItems.ReplaceRange(items);
  234. Search();
  235. }
  236. public void SelectItem([CanBeNull] TItem item)
  237. {
  238. if ((item != null) && !SelectedItems.Contains(item))
  239. {
  240. SelectedItems.Add(item);
  241. Search();
  242. }
  243. }
  244. public void UnselectItem([CanBeNull] TItem item)
  245. {
  246. if ((item != null) && SelectedItems.Contains(item))
  247. {
  248. SelectedItems.Remove(item);
  249. Search();
  250. }
  251. }
  252. public void ToggleSelection(TItem item)
  253. {
  254. if (IsSelected(item))
  255. UnselectItem(item);
  256. else
  257. SelectItem(item);
  258. }
  259. public void SelectNone()
  260. {
  261. SelectedItems.Clear();
  262. Search();
  263. }
  264. public void SelectAll()
  265. {
  266. SelectedItems.ReplaceRange(Items);
  267. Search();
  268. }
  269. void ICoreRepository.SelectItem(object item) => SelectItem(item as TItem);
  270. void ICoreRepository.UnselectItem(object item) => UnselectItem(item as TItem);
  271. void ICoreRepository.ToggleSelection(object item) => ToggleSelection(item as TItem);
  272. bool ICoreRepository.IsSelected(object item) => IsSelected(item as TItem);
  273. void ICoreRepository.SetSelectedItems(IEnumerable<object> items) => SetSelectedItems(items.OfType<TItem>());
  274. #endregion
  275. #region Searching
  276. public Func<TItem, bool> SearchPredicate { get; set; }
  277. public Func<List<TItem>,List<TItem>> SortPredicate { get; set; }
  278. public ICoreRepository Search(Func<TItem, bool> searchpredicate, Func<List<TItem>,List<TItem>> sortpredicate)
  279. {
  280. SortPredicate = sortpredicate;
  281. SearchPredicate = searchpredicate;
  282. Search();
  283. return this;
  284. }
  285. public ICoreRepository Search(Func<TItem, bool> searchpredicate)
  286. {
  287. SearchPredicate = searchpredicate;
  288. Search();
  289. return this;
  290. }
  291. public ICoreRepository Search()
  292. {
  293. var curselected = SelectedItems.ToArray();
  294. var items = AllItems == null
  295. ? new List<TItem>()
  296. : SearchPredicate != null
  297. ? new List<TItem>(AllItems.Where(SearchPredicate))
  298. : new List<TItem>(AllItems);
  299. if (SortPredicate != null)
  300. items = SortPredicate(items);
  301. SelectedItems.ReplaceRange(items.Where(x=>curselected.Contains(x)));
  302. OnPropertyChanged(nameof(SelectedItems));
  303. Items.ReplaceRange(items);
  304. OnPropertyChanged(nameof(Items));
  305. return this;
  306. }
  307. ICoreRepository ICoreRepository.Search(Func<object,bool> method)
  308. => Search((o) => method(o as TItem));
  309. #endregion
  310. protected virtual Expression<Func<TEntity, object>> ImageColumn => null;
  311. #region Loading
  312. private void DoBeforeLoad()
  313. {
  314. _query.Clear();
  315. _query.Add(
  316. EffectiveFilter(),
  317. GetColumns<TItem,TEntity>(),
  318. Sort
  319. );
  320. if (ImageColumn != null)
  321. {
  322. _query.Add(
  323. new Filter<Document>(x => x.ID).InQuery(EffectiveFilter(), ImageColumn),
  324. Columns.None<Document>().Add(x => x.ID)
  325. .Add(x => x.Data)
  326. );
  327. }
  328. }
  329. protected virtual void BeforeLoad(MultiQuery query)
  330. {
  331. }
  332. protected virtual void AfterLoad(MultiQuery query)
  333. {
  334. }
  335. protected void DoLoad()
  336. {
  337. try
  338. {
  339. DoBeforeLoad();
  340. BeforeLoad(_query);
  341. Task.Run(() =>
  342. {
  343. _query.Query();
  344. DoAfterLoad();
  345. }).Wait();
  346. Search();
  347. AfterLoad(_query);
  348. LastUpdated = DateTime.Now;
  349. }
  350. catch (Exception e)
  351. {
  352. MobileLogging.Log(e,"CoreRepository");
  353. }
  354. }
  355. protected void DoAfterLoad()
  356. {
  357. _table = _query.Get<TEntity>();
  358. AllItems.ReplaceRange(_query.Get<TEntity>().Rows.Select(CreateItem<TItem>));
  359. if (ImageColumn != null)
  360. {
  361. Images.Clear();
  362. _query.Get<Document>().IntoDictionary<Document, Guid, byte[]>(Images, x => x.ID,
  363. r => r.Get<Document, byte[]>(x => x.Data));
  364. }
  365. }
  366. #endregion
  367. #region Persistent Storage
  368. protected void InitializeTables()
  369. {
  370. var defs = _query.Definitions();
  371. foreach (var def in defs)
  372. {
  373. var table = InitializeTable(def.Value);
  374. _query.Set(def.Key, table);
  375. }
  376. }
  377. protected CoreTable InitializeTable(IQueryDef def)
  378. {
  379. var table = new CoreTable();
  380. if (def.Columns != null)
  381. table.LoadColumns(def.Columns);
  382. else
  383. table.LoadColumns(def.Type);
  384. return table;
  385. }
  386. protected class QueryStorage : ISerializeBinary
  387. {
  388. private readonly Dictionary<String, CoreTable> _data = new Dictionary<string, CoreTable>();
  389. public CoreTable Get([NotNull] String key) => _data[key];
  390. public void Set([NotNull] String key, CoreTable table) => _data[key] = table;
  391. public bool Contains([NotNull] String key) => _data.ContainsKey(key);
  392. public void SerializeBinary(CoreBinaryWriter writer)
  393. {
  394. writer.Write(_data.Count);
  395. foreach (var key in _data.Keys)
  396. {
  397. writer.Write(key);
  398. _data[key].SerializeBinary(writer);
  399. }
  400. }
  401. public void DeserializeBinary(CoreBinaryReader reader)
  402. {
  403. int count = reader.ReadInt32();
  404. for (int i = 0; i < count; i++)
  405. {
  406. String key = reader.ReadString();
  407. CoreTable table = new CoreTable();
  408. table.DeserializeBinary(reader);
  409. _data[key] = table;
  410. }
  411. }
  412. }
  413. protected bool LoadFromStorage()
  414. {
  415. if (String.IsNullOrWhiteSpace(FileName()))
  416. {
  417. InitializeTables();
  418. return true;
  419. }
  420. var file = CacheFileName(FileName());
  421. if (File.Exists(file))
  422. {
  423. LastUpdated = File.GetLastWriteTime(file);
  424. using (var stream = new FileStream(file, FileMode.Open))
  425. {
  426. QueryStorage storage = Serialization.ReadBinary<QueryStorage>(stream,
  427. BinarySerializationSettings.Latest);
  428. var defs = _query.Definitions();
  429. foreach (var key in defs.Keys)
  430. {
  431. var table = storage.Contains(key.ToString())
  432. ? storage.Get(key.ToString())
  433. : InitializeTable(defs[key]);
  434. if (CheckColumns(table, _query.Definitions()[key].Columns))
  435. _query.Set(key, table);
  436. else
  437. return false;
  438. }
  439. }
  440. }
  441. else
  442. InitializeTables();
  443. return true;
  444. }
  445. private bool CheckColumns(CoreTable table, IColumns required)
  446. {
  447. foreach (var column in required.ColumnNames())
  448. {
  449. if (!table.Columns.Any(x => String.Equals(x.ColumnName, column)))
  450. return false;
  451. }
  452. return true;
  453. }
  454. protected void SaveToStorage()
  455. {
  456. if (String.IsNullOrWhiteSpace(FileName()))
  457. return;
  458. QueryStorage storage = new QueryStorage();
  459. var results = _query.Results();
  460. foreach (var key in results.Keys)
  461. storage.Set(key.ToString(),results[key]);
  462. var data = storage.WriteBinary(BinarySerializationSettings.Latest);
  463. try
  464. {
  465. var file = $"{CacheFileName(FileName())}";
  466. if (!string.IsNullOrWhiteSpace(file))
  467. File.WriteAllBytes(file,data);
  468. }
  469. catch (Exception e)
  470. {
  471. MobileLogging.Log(e);
  472. }
  473. }
  474. #endregion
  475. #region CRUD Operations
  476. public event CoreRepositoryItemCreatedEvent<TItem> ItemAdded;
  477. private T CreateItem<T>(CoreRow row)
  478. where T : Shell<TParent,TEntity>, new()
  479. {
  480. var result = new T() { Row = row, Parent = (TParent)this };
  481. result.PropertyChanged += (_, args) => DoPropertyChanged(result, args);
  482. return result;
  483. }
  484. public virtual TItem CreateItem()
  485. {
  486. CoreRow row = _table.NewRow();
  487. var entity = new TEntity();
  488. _table.FillRow(row,entity);
  489. var result = CreateItem<TItem>(row);
  490. ItemAdded?.Invoke(this, new CoreRepositoryItemCreatedArgs<TItem>(result));
  491. return result;
  492. }
  493. public virtual void CommitItem(TItem item)
  494. {
  495. _table.Rows.Add(item.Row);
  496. AllItems.Add(item);
  497. Search(null);
  498. NotifyChanged();
  499. }
  500. public virtual TItem AddItem()
  501. {
  502. var result = CreateItem();
  503. CommitItem(result);
  504. return result;
  505. }
  506. public virtual void DeleteItem(TItem item)
  507. {
  508. _table.Rows.Remove(item.Row);
  509. AllItems.Remove(item);
  510. Search(null);
  511. NotifyChanged();
  512. }
  513. public virtual void Save(string auditMessage)
  514. {
  515. new Client<TEntity>().Save(Items.Select(x=>x.Entity).Where(x=>x.IsChanged()),auditMessage);
  516. }
  517. object ICoreRepository.CreateItem() => this.CreateItem();
  518. void ICoreRepository.CommitItem(object item)
  519. {
  520. if (item is TItem titem)
  521. CommitItem(titem);
  522. }
  523. object ICoreRepository.AddItem() => this.AddItem();
  524. void ICoreRepository.DeleteItem(object item)
  525. {
  526. if (item is TItem titem)
  527. DeleteItem(titem);
  528. }
  529. #endregion
  530. #region IEnumerable Interface
  531. IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
  532. {
  533. return Items.GetEnumerator();
  534. }
  535. public IEnumerator GetEnumerator()
  536. {
  537. return Items.GetEnumerator();
  538. }
  539. #endregion
  540. }
  541. }