CoreRepository.cs 24 KB

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