CoreRepository.cs 23 KB

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