CoreRepository.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779
  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. var selectedIDs = SelectedItems.Select(x => x.ID).ToHashSet();
  176. Items.Clear();
  177. var dataFileName = DataFileName();
  178. if (!force && !Loaded && CoreRepository.IsCached(dataFileName))
  179. {
  180. DoBeforeLoad();
  181. if (LoadFromStorage())
  182. {
  183. DoAfterLoad();
  184. foreach(var item in Items)
  185. {
  186. item.IsSelected = selectedIDs.Contains(item.ID);
  187. }
  188. return;
  189. }
  190. }
  191. if ((force || !Loaded) && (Host.Status == ConnectionStatus.Connected))
  192. {
  193. DoLoad();
  194. SaveToStorage();
  195. foreach(var item in Items)
  196. {
  197. item.IsSelected = selectedIDs.Contains(item.ID);
  198. }
  199. return;
  200. }
  201. foreach(var item in Items)
  202. {
  203. item.IsSelected = selectedIDs.Contains(item.ID);
  204. }
  205. }
  206. private void AfterRefresh()
  207. {
  208. Loaded = true;
  209. Dispatcher.UIThread.Invoke(Search);
  210. NotifyChanged();
  211. }
  212. public virtual ICoreRepository Refresh(bool force)
  213. {
  214. DoRefresh(force);
  215. AfterRefresh();
  216. return this;
  217. }
  218. public void Refresh(bool force, Action loaded)
  219. {
  220. Task.Run(
  221. () =>
  222. {
  223. DoRefresh(force);
  224. Dispatcher.UIThread.Post(
  225. () =>
  226. {
  227. AfterRefresh();
  228. loaded?.Invoke();
  229. }
  230. );
  231. }
  232. );
  233. }
  234. public Task<ICoreRepository> RefreshAsync(bool force)
  235. {
  236. return Task.Run(() => Refresh(force));
  237. }
  238. public void Reset()
  239. {
  240. Initialize();
  241. }
  242. public event CoreRepositoryChangedEvent? Changed;
  243. protected void NotifyChanged() => Changed?.Invoke(this, new CoreRepositoryChangedEventArgs());
  244. public virtual SortOrder<TEntity>? Sort => LookupFactory.DefineSort<TEntity>();
  245. protected CoreObservableCollection<TItem> AllItems { get; private set; }
  246. private CoreTable _table = new CoreTable();
  247. public CoreObservableCollection<TItem> Items { get; private set; }
  248. public int ItemCount => Items.Count;
  249. IEnumerable ICoreRepository.Items => Items;
  250. #region Item Selection
  251. IEnumerable ICoreRepository.SelectedItems => SelectedItems;
  252. IEnumerable<TItem> SelectedItems => Items.Where(x => x.IsSelected);
  253. public bool IsSelected(TItem? item) => item is not null && item.IsSelected;
  254. public void SetSelectedItems(IEnumerable<TItem> items)
  255. {
  256. var selectedItems = items.Select(x => x.ID).ToHashSet();
  257. foreach(var item in Items)
  258. {
  259. item.IsSelected = selectedItems.Contains(item.ID);
  260. }
  261. Search();
  262. }
  263. public void SelectItem([CanBeNull] TItem? item)
  264. {
  265. if ((item != null) && !item.IsSelected)
  266. {
  267. item.IsSelected = true;
  268. Search();
  269. }
  270. }
  271. public void UnselectItem([CanBeNull] TItem? item)
  272. {
  273. if ((item != null) && item.IsSelected)
  274. {
  275. item.IsSelected = false;
  276. Search();
  277. }
  278. }
  279. public void ToggleSelection(TItem? item)
  280. {
  281. if (IsSelected(item))
  282. UnselectItem(item);
  283. else
  284. SelectItem(item);
  285. }
  286. public void SelectNone()
  287. {
  288. foreach(var item in Items)
  289. {
  290. item.IsSelected = false;
  291. }
  292. Search();
  293. }
  294. public void SelectAll()
  295. {
  296. foreach(var item in Items)
  297. {
  298. item.IsSelected = true;
  299. }
  300. Search();
  301. }
  302. void ICoreRepository.SelectItem(object item) => SelectItem(item as TItem);
  303. void ICoreRepository.UnselectItem(object item) => UnselectItem(item as TItem);
  304. void ICoreRepository.ToggleSelection(object item) => ToggleSelection(item as TItem);
  305. bool ICoreRepository.IsSelected(object item) => IsSelected(item as TItem);
  306. void ICoreRepository.SetSelectedItems(IEnumerable<object> items) => SetSelectedItems(items.OfType<TItem>());
  307. #endregion
  308. #region Searching
  309. public Func<TItem, bool>? SearchPredicate { get; set; }
  310. public Func<List<TItem>,List<TItem>>? SortPredicate { get; set; }
  311. public ICoreRepository Search(Func<TItem, bool> searchpredicate, Func<List<TItem>,List<TItem>> sortpredicate)
  312. {
  313. SortPredicate = sortpredicate;
  314. SearchPredicate = searchpredicate;
  315. Search();
  316. return this;
  317. }
  318. public ICoreRepository Search(Func<TItem, bool> searchpredicate)
  319. {
  320. SearchPredicate = searchpredicate;
  321. Search();
  322. return this;
  323. }
  324. public ICoreRepository Search()
  325. {
  326. var items = AllItems == null
  327. ? new List<TItem>()
  328. : SearchPredicate != null
  329. ? new List<TItem>(AllItems.Where(SearchPredicate))
  330. : new List<TItem>(AllItems);
  331. if (SortPredicate != null)
  332. items = SortPredicate(items);
  333. Items.ReplaceRange(items);
  334. OnPropertyChanged(nameof(Items));
  335. OnPropertyChanged(nameof(ItemCount));
  336. return this;
  337. }
  338. ICoreRepository ICoreRepository.Search(Func<object,bool> method)
  339. => Search((o) => method(o as TItem));
  340. #endregion
  341. protected virtual Expression<Func<TEntity, object?>>? ImageColumn => null;
  342. #region Loading
  343. private void DoBeforeLoad()
  344. {
  345. _query.Clear();
  346. _query.Add(
  347. EffectiveFilter(),
  348. GetColumns<TItem,TEntity>(),
  349. Sort
  350. );
  351. if (ImageColumn is not null)
  352. {
  353. _query.Add(
  354. new Filter<Document>(x => x.ID).InQuery(EffectiveFilter(), ImageColumn),
  355. Columns.None<Document>().Add(x => x.ID)
  356. .Add(x => x.Data)
  357. );
  358. }
  359. }
  360. protected virtual void BeforeLoad(MultiQuery query)
  361. {
  362. }
  363. protected virtual void AfterLoad(MultiQuery query)
  364. {
  365. }
  366. protected void DoLoad()
  367. {
  368. try
  369. {
  370. var selected = AvailableFilters.FirstOrDefault(x => x.Selected)?.Name;
  371. if (!string.IsNullOrWhiteSpace(FilterTag))
  372. {
  373. var filters = new GlobalConfiguration<CoreFilterDefinitions>(FilterTag).Load()
  374. .Where(x => x.Visibility == CoreFilterDefinitionVisibility.DesktopAndMobile)
  375. .Select(x => new CoreRepositoryFilter() { Name = x.Name, Filter = x.Filter, Selected = string.Equals(x.Name,selected) })
  376. .ToList();
  377. if (filters.Any())
  378. filters.Insert(0, new CoreRepositoryFilter() { Name="All", Filter = "", Selected = !filters.Any(x => string.Equals(x.Name, selected)) });
  379. AvailableFilters.ReplaceRange(filters);
  380. Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible)));
  381. }
  382. DoBeforeLoad();
  383. BeforeLoad(_query);
  384. Task.Run(() =>
  385. {
  386. _query.Query();
  387. DoAfterLoad();
  388. }).Wait();
  389. Search();
  390. AfterLoad(_query);
  391. LastUpdated = DateTime.Now;
  392. }
  393. catch (Exception e)
  394. {
  395. MobileLogging.Log(e,"CoreRepository");
  396. }
  397. }
  398. protected void DoAfterLoad()
  399. {
  400. _table = _query.Get<TEntity>();
  401. AllItems.ReplaceRange(_query.Get<TEntity>().Rows.Select(CreateItem<TItem>));
  402. if (ImageColumn != null)
  403. {
  404. Images.Clear();
  405. _query.Get<Document>().IntoDictionary<Document, Guid, byte[]>(Images, x => x.ID,
  406. r => r.Get<Document, byte[]>(x => x.Data));
  407. }
  408. }
  409. #endregion
  410. #region Persistent Storage
  411. protected void InitializeTables()
  412. {
  413. var defs = _query.Definitions();
  414. foreach (var def in defs)
  415. {
  416. var table = InitializeTable(def.Value);
  417. _query.Set(def.Key, table);
  418. }
  419. }
  420. protected CoreTable InitializeTable(IQueryDef def)
  421. {
  422. var table = new CoreTable();
  423. if (def.Columns != null)
  424. table.LoadColumns(def.Columns);
  425. else
  426. table.LoadColumns(def.Type);
  427. return table;
  428. }
  429. protected class QueryStorage : ISerializeBinary
  430. {
  431. private readonly Dictionary<String, CoreTable> _data = new Dictionary<string, CoreTable>();
  432. public CoreTable Get([NotNull] String key) => _data[key];
  433. public void Set([NotNull] String key, CoreTable table) => _data[key] = table;
  434. public bool Contains([NotNull] String key) => _data.ContainsKey(key);
  435. public bool TryGet(string key, [NotNullWhen(true)] out CoreTable? table)
  436. {
  437. return _data.TryGetValue(key, out table);
  438. }
  439. public void SerializeBinary(CoreBinaryWriter writer)
  440. {
  441. writer.Write(_data.Count);
  442. foreach (var key in _data.Keys)
  443. {
  444. writer.Write(key);
  445. _data[key].SerializeBinary(writer);
  446. }
  447. }
  448. public void DeserializeBinary(CoreBinaryReader reader)
  449. {
  450. int count = reader.ReadInt32();
  451. for (int i = 0; i < count; i++)
  452. {
  453. String key = reader.ReadString();
  454. CoreTable table = new CoreTable();
  455. table.DeserializeBinary(reader);
  456. _data[key] = table;
  457. }
  458. }
  459. }
  460. protected bool LoadFromStorage()
  461. {
  462. var filterFileName = FilterFileName();
  463. if (!string.IsNullOrWhiteSpace(filterFileName))
  464. {
  465. filterFileName = CacheFileName(filterFileName);
  466. if (File.Exists(filterFileName))
  467. {
  468. var json = File.ReadAllText(filterFileName);
  469. var filters = Serialization.Deserialize<ObservableCollection<CoreRepositoryFilter>>(json) ?? new ObservableCollection<CoreRepositoryFilter>();
  470. AvailableFilters.ReplaceRange(filters);
  471. Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(FiltersVisible)));
  472. }
  473. }
  474. var dataFileName = DataFileName();
  475. if (String.IsNullOrWhiteSpace(dataFileName))
  476. {
  477. InitializeTables();
  478. return true;
  479. }
  480. dataFileName = CacheFileName(dataFileName);
  481. if (File.Exists(dataFileName))
  482. {
  483. LastUpdated = File.GetLastWriteTime(dataFileName);
  484. using (var stream = new FileStream(dataFileName, FileMode.Open))
  485. {
  486. QueryStorage storage = Serialization.ReadBinary<QueryStorage>(stream,
  487. BinarySerializationSettings.Latest);
  488. var defs = _query.Definitions();
  489. foreach (var key in defs.Keys)
  490. {
  491. var keyStr = key.ToString() ?? "";
  492. if(!storage.TryGet(keyStr, out var table))
  493. {
  494. table = InitializeTable(defs[key]);
  495. }
  496. var queryDef = _query.Definitions()[key];
  497. if (CheckColumns(table, queryDef.Type, queryDef.Columns))
  498. _query.Set(key, table);
  499. else
  500. return false;
  501. }
  502. }
  503. }
  504. else
  505. InitializeTables();
  506. return true;
  507. }
  508. private bool CheckColumns(CoreTable table, Type T, IColumns? required)
  509. {
  510. required = CoreUtils.GetColumns(T, required);
  511. foreach (var column in required.ColumnNames())
  512. {
  513. if (!table.Columns.Any(x => String.Equals(x.ColumnName, column)))
  514. return false;
  515. }
  516. return true;
  517. }
  518. protected void SaveToStorage()
  519. {
  520. var filterFileName = FilterFileName();
  521. if (!string.IsNullOrWhiteSpace(filterFileName))
  522. {
  523. filterFileName = CacheFileName(filterFileName);
  524. var json = Serialization.Serialize(AvailableFilters);
  525. File.WriteAllText(filterFileName,json);
  526. }
  527. var dataFileName = DataFileName();
  528. if (String.IsNullOrWhiteSpace(dataFileName))
  529. return;
  530. QueryStorage storage = new QueryStorage();
  531. var results = _query.Results();
  532. foreach (var key in results.Keys)
  533. storage.Set(key.ToString() ?? "", results[key]);
  534. var data = storage.WriteBinary(BinarySerializationSettings.Latest);
  535. try
  536. {
  537. var file = CacheFileName(dataFileName);
  538. File.WriteAllBytes(file,data);
  539. }
  540. catch (Exception e)
  541. {
  542. MobileLogging.Log(e);
  543. }
  544. }
  545. #endregion
  546. #region CRUD Operations
  547. public event CoreRepositoryItemCreatedEvent<TItem>? ItemAdded;
  548. private T CreateItem<T>(CoreRow row)
  549. where T : Shell<TParent,TEntity>, new()
  550. {
  551. var result = new T() { Row = row, Parent = (TParent)this };
  552. result.PropertyChanged += (_, args) => DoPropertyChanged(result, args);
  553. return result;
  554. }
  555. public virtual TItem CreateItem()
  556. {
  557. CoreRow row = _table.NewRow();
  558. var entity = new TEntity();
  559. _table.FillRow(row,entity);
  560. var result = CreateItem<TItem>(row);
  561. ItemAdded?.Invoke(this, new CoreRepositoryItemCreatedArgs<TItem>(result));
  562. return result;
  563. }
  564. public virtual void CommitItem(TItem item)
  565. {
  566. _table.Rows.Add(item.Row);
  567. AllItems.Add(item);
  568. Search();
  569. NotifyChanged();
  570. }
  571. public virtual TItem AddItem()
  572. {
  573. var result = CreateItem();
  574. CommitItem(result);
  575. return result;
  576. }
  577. public virtual void DeleteItem(TItem item)
  578. {
  579. _table.Rows.Remove(item.Row);
  580. AllItems.Remove(item);
  581. Search();
  582. NotifyChanged();
  583. }
  584. public virtual void Save(string auditMessage)
  585. {
  586. new Client<TEntity>().Save(Items.Select(x=>x.Entity).Where(x=>x.IsChanged()),auditMessage);
  587. }
  588. public virtual Task SaveAsync(string auditMessage)
  589. {
  590. return Task.Run(() => Save(auditMessage));
  591. }
  592. object ICoreRepository.CreateItem() => this.CreateItem();
  593. void ICoreRepository.CommitItem(object item)
  594. {
  595. if (item is TItem titem)
  596. CommitItem(titem);
  597. }
  598. object ICoreRepository.AddItem() => this.AddItem();
  599. void ICoreRepository.DeleteItem(object item)
  600. {
  601. if (item is TItem titem)
  602. DeleteItem(titem);
  603. }
  604. #endregion
  605. #region IEnumerable Interface
  606. IEnumerator<TItem> IEnumerable<TItem>.GetEnumerator()
  607. {
  608. return Items.GetEnumerator();
  609. }
  610. public IEnumerator GetEnumerator()
  611. {
  612. return Items.GetEnumerator();
  613. }
  614. #endregion
  615. }
  616. }