using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Helpers; using AyCode.Core.Interfaces; using AyCode.Services.SignalRs; using System.Collections; using System.Collections.ObjectModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using AyCode.Core.Serializers.Jsons; namespace AyCode.Services.Server.SignalRs { //public class TrackingItemGuid(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem(trackingState, currentValue, originalValue) // where TDataItem : class, IId { } //public class TrackingItemInt(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) : TrackingItem(trackingState, currentValue, originalValue) // where TDataItem : class, IId {} public class TrackingItem(TrackingState trackingState, TDataItem currentValue, TDataItem? originalValue = null) where TDataItem : class, IId where TId : struct { public TrackingState TrackingState { get; internal set; } = trackingState; public TDataItem CurrentValue { get; internal set; } = currentValue; public TDataItem? OriginalValue { get; init; } = originalValue; internal TrackingItem UpdateItem(TrackingState trackingState, TDataItem newValue) //where TTrackingItem : TrackingItem { CurrentValue = newValue; if (TrackingState != TrackingState.Add) TrackingState = trackingState; return (TrackingItem)this; } } //public class ChangeTrackingGuid : ChangeTracking /*: IEnumerable>*/ where TDataItem : class, IId //{ // protected override bool HasIdValue(TDataItem dataItem) => !dataItem.Id.IsNullOrEmpty(); // protected override int FindIndex(TDataItem newValue) => TrackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id); // public override bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) // { // trackingItem = TrackingItems.FirstOrDefault(x => x.CurrentValue.Id == id); // return trackingItem != null; // } //} //public class ChangeTrackingInt : ChangeTracking /*: IEnumerable>*/ where TDataItem : class, IId //{ // protected override bool HasIdValue(TDataItem dataItem) => true;//dataItem.Id.IsNullOrEmpty(); // protected override int FindIndex(TDataItem newValue) => TrackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id); // public override bool TryGetTrackingItem(int id, [NotNullWhen(true)] out TrackingItem? trackingItem) // { // trackingItem = TrackingItems.FirstOrDefault(x => x.CurrentValue.Id == id); // return trackingItem != null; // } //} public class ChangeTracking /*: IEnumerable>*/where TDataItem : class, IId where TId : struct { private readonly EqualityComparer _equalityComparerId = EqualityComparer.Default; private readonly List> _trackingItems = []; //TODO: Dictionary... - J. //protected abstract bool HasIdValue(TDataItem dataItem); //protected abstract int FindIndex(TDataItem newValue); //public abstract bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem? trackingItem); private bool HasIdValue(TDataItem dataItem) => !_equalityComparerId.Equals(dataItem.Id, default);//dataItem.Id.IsNullOrEmpty(); public int FindIndex(TDataItem newValue) => _trackingItems.FindIndex(x => _equalityComparerId.Equals(x.CurrentValue.Id, newValue.Id)); public bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem? trackingItem) { trackingItem = _trackingItems.FirstOrDefault(x => _equalityComparerId.Equals(x.CurrentValue.Id, id)); return trackingItem != null; } internal TrackingItem? AddTrackingItem(TrackingState trackingState, TDataItem newValue, TDataItem? originalValue = null) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), $@"currentValue.Id.IsNullOrEmpty()"); var itemIndex = FindIndex(newValue); //_trackingItems.FindIndex(x => x.CurrentValue.Id == newValue.Id); TrackingItem? trackingItem = null; if (itemIndex > -1) { trackingItem = _trackingItems[itemIndex]; if (trackingState == TrackingState.Remove && trackingItem.TrackingState == TrackingState.Add) { _trackingItems.RemoveAt(itemIndex); return null; } return trackingItem.UpdateItem(trackingState, newValue); } if (originalValue != null && Equals(newValue, originalValue)) originalValue = TrackingItemHelpers.JsonClone(originalValue); //trackingItem = new TrackingItem(trackingState, newValue, originalValue); trackingItem = new TrackingItem(trackingState, newValue, originalValue); _trackingItems.Add(trackingItem); return trackingItem; } public int Count => _trackingItems.Count; internal void Clear() => _trackingItems.Clear(); public List> ToList() => _trackingItems.ToList(); internal void Remove(TrackingItem trackingItem) => _trackingItems.Remove(trackingItem); //public IEnumerator> GetEnumerator() //{ // return _trackingItems.GetEnumerator(); //} //IEnumerator IEnumerable.GetEnumerator() //{ // return GetEnumerator(); //} } //[Serializable] //[DebuggerDisplay("Count = {Count}")] //public abstract class AcSignalRDataSourceGuid : AcSignalRDataSource // where TDataItem : class, IId where TIList : class, IList //{ // public AcSignalRDataSourceGuid(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null) // : base(signalRClient, signalRCrudTags, contextIds) // { // } // protected override bool HasIdValue(TDataItem dataItem) => !dataItem.Id.IsNullOrEmpty(); // protected override bool IdEquals(Guid id1, Guid id2) => id1 == id2; // protected override int FindIndexInnerList(Guid id) => InnerList.FindIndex(x => x.Id == id); // protected override TDataItem? FirstOrDefaultInnerList(Guid id) => InnerList.FirstOrDefault(x => x.Id == id); //} //[Serializable] //[DebuggerDisplay("Count = {Count}")] //public abstract class AcSignalRDataSourceInt : AcSignalRDataSource // where TDataItem : class, IId where TIList : class, IList //{ // public AcSignalRDataSourceInt(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null) // : base(signalRClient, signalRCrudTags, contextIds) // { // } // protected override bool HasIdValue(TDataItem dataItem) => true; // protected override bool IdEquals(int id1, int id2) => id1 == id2; // protected override int FindIndexInnerList(int id) => InnerList.FindIndex(x => x.Id == id); // protected override TDataItem? FirstOrDefaultInnerList(int id) => InnerList.FirstOrDefault(x => x.Id == id); //} [Serializable] [DebuggerDisplay("Count = {Count}")] public abstract class AcSignalRDataSource : IList, IList, IReadOnlyList where TDataItem : class, IId where TId : struct where TIList : class, IList { private readonly object _syncRoot = new(); private readonly SemaphoreSlim _asyncLock = new(1, 1); private readonly EqualityComparer _equalityComparerId = EqualityComparer.Default; protected TIList InnerList = Activator.CreateInstance(); protected readonly ChangeTracking TrackingItems = new(); public object[]? ContextIds; public string? FilterText { get; set; } public AcSignalRClientBase SignalRClient; protected readonly SignalRCrudTags SignalRCrudTags; public Func, Task>? OnDataSourceItemChanged; public Func? OnDataSourceLoaded; /// /// Event fired when synchronization state changes (true = syncing started, false = syncing ended) /// public event Action? OnSyncingStateChanged; private int _activeSyncOperations; /// /// Indicates whether any synchronization operation is in progress /// public bool IsSyncing => _activeSyncOperations > 0; private void BeginSync() { var wasZero = Interlocked.Increment(ref _activeSyncOperations) == 1; if (wasZero) OnSyncingStateChanged?.Invoke(true); } private void EndSync() { var isZero = Interlocked.Decrement(ref _activeSyncOperations) == 0; if (isZero) OnSyncingStateChanged?.Invoke(false); } protected bool HasIdValue(TDataItem dataItem) => !_equalityComparerId.Equals(dataItem.Id, default); protected bool IdEquals(TId id1, TId id2) => _equalityComparerId.Equals(id1, id2); protected int FindIndexInnerListUnsafe(TId id) => InnerList.FindIndex(x => IdEquals(x.Id, id)); protected TDataItem? FirstOrDefaultInnerListUnsafe(TId id) => InnerList.FirstOrDefault(x => IdEquals(x.Id, id)); public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, object[]? contextIds = null) { ContextIds = contextIds; SignalRCrudTags = signalRCrudTags; SignalRClient = signalRClient; } public bool IsSynchronized => true; public object SyncRoot => _syncRoot; public bool IsFixedSize => false; public bool HasWorkingReferenceList { get; private set; } public void SetWorkingReferenceList(TIList? workingIList) { if (workingIList == null) return; lock (_syncRoot) { HasWorkingReferenceList = true; if (ReferenceEquals(InnerList, workingIList)) return; if (workingIList.Count == 0) AddRangeUnsafe(InnerList, workingIList); ClearUnsafe(true); InnerList = workingIList; } } public TIList GetReferenceInnerList() => InnerList; private object[]? GetContextParams() { var parameters = new List(); if (ContextIds != null) parameters.AddRange(ContextIds); if (FilterText != null) parameters.Add(FilterText); if (parameters.Count == 0) parameters = null; return parameters?.ToArray(); } #region Load Methods /// /// GetAllMessageTag - Synchronous wait version /// public async Task LoadDataSource(bool clearChangeTracking = true) { if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); BeginSync(); try { var responseData = (await SignalRClient.GetAllAsync(SignalRCrudTags.GetAllMessageTag, GetContextParams())) ?? throw new NullReferenceException(); await LoadDataSource(responseData, false, false, clearChangeTracking); } finally { EndSync(); } } /// /// GetAllMessageTag - Async callback version with optimized direct populate. /// Uses SignalResponseDataMessage to avoid double deserialization. /// public Task LoadDataSourceAsync(bool clearChangeTracking = true) { if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); BeginSync(); // Request SignalResponseDataMessage directly to avoid deserializing ResponseData return SignalRClient.GetAllAsync(SignalRCrudTags.GetAllMessageTag, GetContextParams()) .ContinueWith(async task => { try { var response = task.Result; if (response?.Status != SignalResponseStatus.Success || response.ResponseData == null) throw new NullReferenceException($"LoadDataSourceAsync; Status: {response?.Status}"); await LoadDataSourceFromResponseData(response.ResponseData, response.DataSerializerType, false, false, clearChangeTracking); } finally { EndSync(); } }).Unwrap(); } /// /// Loads data source directly from ResponseData byte[], avoiding double deserialization. /// public async Task LoadDataSourceFromResponseData(byte[] responseData, AcSerializerType serializerType, bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true) { await _asyncLock.WaitAsync(); try { if (!setSourceToWorkingReferenceList) { // Direct populate into existing InnerList if (serializerType == AcSerializerType.Binary) { if (InnerList is IAcObservableCollection observable) { observable.BeginUpdate(); try { responseData.BinaryToMerge(InnerList); } finally { observable.EndUpdate(); } } else { responseData.BinaryTo(InnerList); } } else { // JSON mode var json = System.Text.Encoding.UTF8.GetString(responseData); if (InnerList is IAcObservableCollection observable) { observable.PopulateFromJson(json); } else { json.JsonTo(InnerList); } } } else { // Deserialize to new list and set as reference TIList? fromSource; if (serializerType == AcSerializerType.Binary) fromSource = responseData.BinaryTo(); else fromSource = System.Text.Encoding.UTF8.GetString(responseData).JsonTo(); if (fromSource != null) { ClearUnsafe(clearChangeTracking); SetWorkingReferenceListUnsafe(fromSource); } } if (clearChangeTracking) TrackingItems.Clear(); if (refreshDataFromDbAsync) { LoadDataSourceAsync(false).Forget(); return; } } finally { _asyncLock.Release(); } if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke(); } public async Task LoadDataSource(TIList fromSource, bool refreshDataFromDbAsync = false, bool setSourceToWorkingReferenceList = false, bool clearChangeTracking = true) { await _asyncLock.WaitAsync(); try { if (!ReferenceEquals(InnerList, fromSource)) { if (!setSourceToWorkingReferenceList) { // CopyTo uses JSON serialization which already handles BeginUpdate/EndUpdate // for IAcObservableCollection via AcJsonDeserializer.Populate fromSource.CopyTo(InnerList); } else { ClearUnsafe(clearChangeTracking); SetWorkingReferenceListUnsafe(fromSource); } } else if (clearChangeTracking) TrackingItems.Clear(); if (refreshDataFromDbAsync) { LoadDataSourceAsync(false).Forget(); return; } } finally { _asyncLock.Release(); } if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke(); } public async Task LoadItem(TId id) { if (SignalRCrudTags.GetItemMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None"); var resultitem = await SignalRClient.GetByIdAsync(SignalRCrudTags.GetItemMessageTag, id); if (resultitem == null) return null; await _asyncLock.WaitAsync(); try { var index = FindIndexInnerListUnsafe(id); if (index >= 0) resultitem.CopyTo(InnerList[index]); else InnerList.Add(resultitem); } finally { _asyncLock.Release(); } var eventArgs = new ItemChangedEventArgs(resultitem, TrackingState.Get); if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); return resultitem; } #endregion #region Indexer public TDataItem this[int index] { get { lock (_syncRoot) { if ((uint)index >= (uint)InnerList.Count) throw new ArgumentOutOfRangeException(nameof(index)); return InnerList[index]; } } set { lock (_syncRoot) { UpdateUnsafe(index, value); } } } #endregion #region Add Methods public void Add(TDataItem newValue) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"Add->HasIdValue(newValue) == false"); lock (_syncRoot) { if (ContainsUnsafe(newValue)) throw new ArgumentException($@"It already contains this Id! {newValue}", nameof(newValue)); AddUnsafe(newValue); } } public async Task Add(TDataItem newValue, bool autoSave) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"Add->HasIdValue(newValue) == false"); await _asyncLock.WaitAsync(); try { if (ContainsUnsafe(newValue)) throw new ArgumentException($@"It already contains this Id! {newValue}", nameof(newValue)); AddUnsafe(newValue); } finally { _asyncLock.Release(); } return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; } public async Task AddOrUpdate(TDataItem newValue, bool autoSave) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()"); int index; await _asyncLock.WaitAsync(); try { index = FindIndexInnerListUnsafe(newValue.Id); if (index > -1) { UpdateUnsafe(index, newValue); } else { AddUnsafe(newValue); } } finally { _asyncLock.Release(); } return autoSave ? await SaveItem(newValue, index > -1 ? TrackingState.Update : TrackingState.Add) : newValue; } private void AddUnsafe(TDataItem newValue) { TrackingItems.AddTrackingItem(TrackingState.Add, newValue); InnerList.Add(newValue); } public void AddRange(IEnumerable source) { lock (_syncRoot) { AddRangeUnsafe(source, InnerList); } } private void AddRangeUnsafe(IEnumerable source, TIList destination) { switch (destination) { case IAcObservableCollection dest: dest.AddRange(source); break; case List dest: dest.AddRange(source); break; default: foreach (var dataItem in source) destination.Add(dataItem); break; } } #endregion #region Insert Methods public void Insert(int index, TDataItem newValue) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); lock (_syncRoot) { if (ContainsUnsafe(newValue)) throw new ArgumentException($@"Insert; It already contains this Id! {newValue}", nameof(newValue)); TrackingItems.AddTrackingItem(TrackingState.Add, newValue); InnerList.Insert(index, newValue); } } public async Task Insert(int index, TDataItem newValue, bool autoSave) { if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); await _asyncLock.WaitAsync(); try { if (ContainsUnsafe(newValue)) throw new ArgumentException($@"Insert; It already contains this Id! {newValue}", nameof(newValue)); TrackingItems.AddTrackingItem(TrackingState.Add, newValue); InnerList.Insert(index, newValue); } finally { _asyncLock.Release(); } return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; } #endregion #region Update Methods public Task Update(TDataItem newItem, bool autoSave) { int index; lock (_syncRoot) { index = FindIndexInnerListUnsafe(newItem.Id); } return Update(index, newItem, autoSave); } public async Task Update(int index, TDataItem newValue, bool autoSave) { await _asyncLock.WaitAsync(); try { UpdateUnsafe(index, newValue); } finally { _asyncLock.Release(); } return autoSave ? await SaveItem(newValue, TrackingState.Update) : newValue; } private void UpdateUnsafe(int index, TDataItem newValue) { if (default(TDataItem) != null && newValue == null) throw new NullReferenceException(nameof(newValue)); if (!HasIdValue(newValue)) throw new ArgumentNullException(nameof(newValue), @"UpdateUnsafe->newValue.Id.IsNullOrEmpty()"); if ((uint)index >= (uint)InnerList.Count) throw new ArgumentOutOfRangeException(nameof(index)); var currentItem = InnerList[index]; if (!IdEquals(currentItem.Id, newValue.Id)) throw new ArgumentException($@"UpdateUnsafe; currentItem.Id != item.Id! {newValue}", nameof(newValue)); TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem); InnerList[index] = newValue; } #endregion #region Remove Methods public bool Remove(TDataItem item) { lock (_syncRoot) { var index = FindIndexInnerListUnsafe(item.Id); if (index < 0) return false; RemoveAtUnsafe(index); return true; } } public async Task Remove(TId id, bool autoSave) { TDataItem? item; await _asyncLock.WaitAsync(); try { item = FirstOrDefaultInnerListUnsafe(id); if (item == null) return true; var index = FindIndexInnerListUnsafe(id); if (index < 0) return false; RemoveAtUnsafe(index); } finally { _asyncLock.Release(); } if (autoSave) { await SaveItem(item, TrackingState.Remove); } return true; } public async Task Remove(TDataItem item, bool autoSave) { bool result; await _asyncLock.WaitAsync(); try { var index = FindIndexInnerListUnsafe(item.Id); if (index < 0) { result = false; } else { RemoveAtUnsafe(index); result = true; } } finally { _asyncLock.Release(); } if (autoSave && result) { await SaveItem(item, TrackingState.Remove); } return result; } public bool TryRemove(TId id, out TDataItem? item) { lock (_syncRoot) { item = FirstOrDefaultInnerListUnsafe(id); if (item == null) return false; var index = FindIndexInnerListUnsafe(id); if (index < 0) return false; RemoveAtUnsafe(index); return true; } } public void RemoveAt(int index) { lock (_syncRoot) { RemoveAtUnsafe(index); } } private void RemoveAtUnsafe(int index) { var currentItem = InnerList[index]; if (!HasIdValue(currentItem)) throw new ArgumentNullException(nameof(currentItem), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}"); TrackingItems.AddTrackingItem(TrackingState.Remove, currentItem, currentItem); InnerList.RemoveAt(index); } public async Task RemoveAt(int index, bool autoSave) { TDataItem currentItem; await _asyncLock.WaitAsync(); try { currentItem = InnerList[index]; RemoveAtUnsafe(index); } finally { _asyncLock.Release(); } if (autoSave) { await SaveItem(currentItem, TrackingState.Remove); } } #endregion #region Tracking Methods public List> GetTrackingItems() { lock (_syncRoot) { return TrackingItems.ToList(); } } public void SetTrackingStateToUpdate(TDataItem item) { lock (_syncRoot) { if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem)) { if (trackingItem.TrackingState != TrackingState.Add) trackingItem.TrackingState = TrackingState.Update; return; } var originalItem = FirstOrDefaultInnerListUnsafe(item.Id); if (originalItem == null) return; TrackingItems.AddTrackingItem(TrackingState.Update, item, originalItem); } } public bool TryGetTrackingItem(TId id, [NotNullWhen(true)] out TrackingItem? trackingItem) { lock (_syncRoot) { return TrackingItems.TryGetTrackingItem(id, out trackingItem); } } #endregion #region Save Methods public async Task>> SaveChanges() { BeginSync(); try { List> itemsToSave; await _asyncLock.WaitAsync(); try { itemsToSave = TrackingItems.ToList(); } finally { _asyncLock.Release(); } foreach (var trackingItem in itemsToSave) { try { await SaveTrackingItemUnsafe(trackingItem); } catch (Exception) { TryRollbackItem(trackingItem.CurrentValue.Id, out _); } } lock (_syncRoot) { return TrackingItems.ToList(); } } finally { EndSync(); } } public async Task SaveChangesAsync() { BeginSync(); try { List> itemsToSave; await _asyncLock.WaitAsync(); try { itemsToSave = TrackingItems.ToList(); } finally { _asyncLock.Release(); } foreach (var trackingItem in itemsToSave) { try { await SaveTrackingItemUnsafeAsync(trackingItem); } catch (Exception) { TryRollbackItem(trackingItem.CurrentValue.Id, out _); } } } finally { EndSync(); } } public async Task SaveItem(TId id) { TrackingItem? trackingItem; await _asyncLock.WaitAsync(); try { if (!TrackingItems.TryGetTrackingItem(id, out trackingItem)) throw new NullReferenceException($"SaveItem; trackingItem not found for id: {id}"); } finally { _asyncLock.Release(); } return await SaveTrackingItemUnsafe(trackingItem); } public async Task SaveItem(TId id, TrackingState trackingState) { TDataItem? item; lock (_syncRoot) { item = FirstOrDefaultInnerListUnsafe(id); } if (item == null) throw new NullReferenceException($"SaveItem; item not found for id: {id}"); return await SaveItem(item, trackingState); } public Task SaveItem(TDataItem item, TrackingState trackingState) => SaveItemUnsafe(item, trackingState); private Task SaveTrackingItemUnsafe(TrackingItem trackingItem) => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState); private async Task SaveTrackingItemUnsafeAsync(TrackingItem trackingItem) => SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState); private Task SaveItemUnsafe(TDataItem item, TrackingState trackingState) { var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); if (messageTag == AcSignalRTags.None) throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None"); return SignalRClient.PostDataAsync(messageTag, item).ContinueWith(task => { if (task.Result == null) { if (TryRollbackItem(item.Id, out _)) return item; throw new NullReferenceException($"SaveItemUnsafe; result == null"); } ProcessSavedResponseItem(task.Result, trackingState, item.Id); return task.Result; }, TaskScheduler.Default); } /// /// Saves item in background (fire-and-forget friendly). Does not block UI. /// private Task SaveItemUnsafeAsync(TDataItem item, TrackingState trackingState) { var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); if (messageTag == AcSignalRTags.None) return Task.CompletedTask; return SignalRClient.PostDataAsync(messageTag, item, response => { if (response.Status != SignalResponseStatus.Success || response.ResponseData == null) { if (TryRollbackItem(item.Id, out _)) return; throw new NullReferenceException($"SaveItemUnsafeAsync; Status: {response.Status}"); } var resultItem = response.GetResponseData(); ProcessSavedResponseItem(resultItem, trackingState, item.Id); }); } private Task ProcessSavedResponseItem(TDataItem? resultItem, TrackingState trackingState, TId originalId) { if (resultItem == null) return Task.CompletedTask; lock (_syncRoot) { if (TrackingItems.TryGetTrackingItem(originalId, out var trackingItem)) TrackingItems.Remove(trackingItem); var index = FindIndexInnerListUnsafe(originalId); if (index >= 0) { resultItem.CopyTo(InnerList[index]); } } var eventArgs = new ItemChangedEventArgs(resultItem, trackingState); if (OnDataSourceItemChanged != null) return OnDataSourceItemChanged.Invoke(eventArgs); return Task.CompletedTask; } #endregion #region Rollback Methods private void RollbackItemUnsafe(TrackingItem trackingItem) { var index = FindIndexInnerListUnsafe(trackingItem.CurrentValue.Id); if (index >= 0) { if (trackingItem.TrackingState == TrackingState.Add) InnerList.RemoveAt(index); else trackingItem.OriginalValue!.CopyTo(InnerList[index]); } else if (trackingItem.TrackingState != TrackingState.Add) { InnerList.Add(trackingItem.OriginalValue!); } TrackingItems.Remove(trackingItem); } public bool TryRollbackItem(TId id, out TDataItem? originalValue) { lock (_syncRoot) { if (TrackingItems.TryGetTrackingItem(id, out var trackingItem)) { originalValue = trackingItem.OriginalValue; RollbackItemUnsafe(trackingItem); return true; } originalValue = null; return false; } } public void Rollback() { lock (_syncRoot) { foreach (var trackingItem in TrackingItems.ToList()) RollbackItemUnsafe(trackingItem); } } #endregion #region Collection Properties and Methods public int Count { get { lock (_syncRoot) { return InnerList.Count; } } } public void Clear() => Clear(true); public void Clear(bool clearChangeTracking) { lock (_syncRoot) { ClearUnsafe(clearChangeTracking); } } private void ClearUnsafe(bool clearChangeTracking) { if (clearChangeTracking) TrackingItems.Clear(); InnerList.Clear(); } public int IndexOf(TId id) { lock (_syncRoot) { return FindIndexInnerListUnsafe(id); } } public int IndexOf(TDataItem item) => IndexOf(item.Id); public bool TryGetIndex(TId id, out int index) => (index = IndexOf(id)) > -1; public bool Contains(TDataItem item) => IndexOf(item) > -1; private bool ContainsUnsafe(TDataItem item) => FindIndexInnerListUnsafe(item.Id) > -1; public bool TryGetValue(TId id, [NotNullWhen(true)] out TDataItem? item) { lock (_syncRoot) { item = FirstOrDefaultInnerListUnsafe(id); return item != null; } } public void CopyTo(TDataItem[] array) => CopyTo(array, 0); public void CopyTo(TDataItem[] array, int arrayIndex) { lock (_syncRoot) { InnerList.CopyTo(array, arrayIndex); } } public int BinarySearch(int index, int count, TDataItem item, IComparer? comparer) { throw new NotImplementedException($"BinarySearch"); } public int BinarySearch(TDataItem item) => BinarySearch(0, Count, item, null); public int BinarySearch(TDataItem item, IComparer? comparer) => BinarySearch(0, Count, item, comparer); public IEnumerator GetEnumerator() { lock (_syncRoot) { // Return a copy to avoid modification during enumeration return InnerList.ToList().GetEnumerator(); } } public ReadOnlyCollection AsReadOnly() => new(this); private static bool IsCompatibleObject(object? value) => (value is TDataItem) || (value == null && default(TDataItem) == null); private void SetWorkingReferenceListUnsafe(TIList workingIList) { HasWorkingReferenceList = true; if (ReferenceEquals(InnerList, workingIList)) return; if (workingIList.Count == 0) AddRangeUnsafe(InnerList, workingIList); ClearUnsafe(true); InnerList = workingIList; } #endregion #region IList, ICollection Interface Implementation bool IList.IsReadOnly => false; object? IList.this[int index] { get => this[index]; set { if (default(TDataItem) != null && value == null) throw new NullReferenceException(nameof(value)); try { this[index] = (TDataItem)value!; } catch (InvalidCastException) { throw new InvalidCastException(nameof(value)); } } } int IList.Add(object? item) { if (default(TDataItem) != null && item == null) throw new NullReferenceException(nameof(item)); try { Add((TDataItem)item!); } catch (InvalidCastException) { throw new InvalidCastException(nameof(item)); } return Count - 1; } void IList.Clear() => Clear(true); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); bool IList.Contains(object? item) => IsCompatibleObject(item) && Contains((TDataItem)item!); int IList.IndexOf(object? item) => IsCompatibleObject(item) ? IndexOf((TDataItem)item!) : -1; void IList.Insert(int index, object? item) { if (default(TDataItem) != null && item == null) throw new NullReferenceException(nameof(item)); try { Insert(index, (TDataItem)item!); } catch (InvalidCastException) { throw new InvalidCastException(nameof(item)); } } void IList.Remove(object? item) { if (IsCompatibleObject(item)) Remove((TDataItem)item!); } void ICollection.Clear() => Clear(true); void ICollection.CopyTo(Array array, int arrayIndex) { if (array != null && array.Rank != 1) throw new ArgumentException("Multi-dimensional arrays not supported"); try { lock (_syncRoot) { Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); } } catch (ArrayTypeMismatchException) { throw new ArrayTypeMismatchException(); } } int ICollection.Count => Count; int ICollection.Count => Count; bool ICollection.IsReadOnly => false; void IList.RemoveAt(int index) => RemoveAt(index); int IReadOnlyCollection.Count => Count; #endregion } public class ItemChangedEventArgs where T : class { internal ItemChangedEventArgs(T item, TrackingState trackingState) { Item = item; TrackingState = trackingState; } public T Item { get; } public TrackingState TrackingState { get; } } }