diff --git a/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor b/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor index 6401fa80..8cb19deb 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor @@ -396,7 +396,7 @@ { _logger.Info("DataSourceItemChanged called"); - AppointmentModels.UpdateCollection(CreateAppointmentModel(args.DataItem), args.DataChangeMode == DataChangeMode.Remove); + AppointmentModels.UpdateCollection(CreateAppointmentModel(args.DataItem), args.TrackingState == TrackingState.Remove); } private void DataItemSaving(GridEditModelSavingEventArgs e) diff --git a/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor index 4984cf08..83b676ac 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor @@ -59,7 +59,7 @@ private void DataItemChanged(GridDataItemChangedEventArgs args) { - ParentData.TransferToDrivers.UpdateCollection(args.DataItem, args.DataChangeMode == DataChangeMode.Remove); + ParentData.TransferToDrivers.UpdateCollection(args.DataItem, args.TrackingState == TrackingState.Remove); OnTransferToDriverChanged.InvokeAsync(args.DataItem); } diff --git a/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs b/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs index 356e92f0..0fc18741 100644 --- a/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs +++ b/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs @@ -17,7 +17,7 @@ namespace TIAMSharedUI.Shared.Components.Grids { public class GridDataItemChangingEventArgs : GridDataItemChangedEventArgs where TDataItem : class, IId { - internal GridDataItemChangingEventArgs(TiamGrid grid, TDataItem dataItem, DataChangeMode dataChangeMode) : base(grid, dataItem, dataChangeMode) + internal GridDataItemChangingEventArgs(TiamGrid grid, TDataItem dataItem, TrackingState trackingState) : base(grid, dataItem, trackingState) { } @@ -26,16 +26,16 @@ namespace TIAMSharedUI.Shared.Components.Grids public class GridDataItemChangedEventArgs where TDataItem : class, IId { - internal GridDataItemChangedEventArgs(TiamGrid grid, TDataItem dataItem, DataChangeMode dataChangeMode) + internal GridDataItemChangedEventArgs(TiamGrid grid, TDataItem dataItem, TrackingState trackingState) { Grid = grid; DataItem = dataItem; - DataChangeMode = dataChangeMode; + TrackingState = trackingState; } public TiamGrid Grid { get; } public TDataItem DataItem { get; } - public DataChangeMode DataChangeMode { get; } + public TrackingState TrackingState { get; } } public class TiamGrid : DxGrid where TDataItem : class, IId @@ -121,13 +121,13 @@ namespace TIAMSharedUI.Shared.Components.Grids } public Task AddDataItem(TDataItem dataItem) => AddDataItem(dataItem, AddMessageTag); - public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Add); + public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Add); public Task UpdateDataItem(TDataItem dataItem) => UpdateDataItem(dataItem, UpdateMessageTag); - public Task UpdateDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Update); + public Task UpdateDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Update); public Task RemoveDataItem(TDataItem dataItem) => RemoveDataItem(dataItem, RemoveMessageTag); - public Task RemoveDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Remove); + public Task RemoveDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Remove); public Task RemoveDataItem(Guid id) => RemoveDataItem(id, RemoveMessageTag); @@ -192,9 +192,9 @@ namespace TIAMSharedUI.Shared.Components.Grids }); } - protected virtual async Task PostDataToServerAsync(TDataItem dataItem, int messageTag, DataChangeMode dataChangeMode) + protected virtual async Task PostDataToServerAsync(TDataItem dataItem, int messageTag, TrackingState trackingState) { - var changingEventArgs = new GridDataItemChangingEventArgs(this, dataItem, dataChangeMode); + var changingEventArgs = new GridDataItemChangingEventArgs(this, dataItem, trackingState); await OnDataItemChanging.InvokeAsync(changingEventArgs); if (changingEventArgs.IsCanceled) @@ -209,7 +209,7 @@ namespace TIAMSharedUI.Shared.Components.Grids if (dataItem.Id.IsNullOrEmpty()) dataItem.Id = Guid.NewGuid(); - _dataSource.UpdateCollection(dataItem, dataChangeMode == DataChangeMode.Remove); //egyből látszódik a változás a grid-ben, nem csak a callback lefutásakor! felhasználóbarátabb... - J. + _dataSource.UpdateCollection(dataItem, trackingState == TrackingState.Remove); //egyből látszódik a változás a grid-ben, nem csak a callback lefutásakor! felhasználóbarátabb... - J. SignalRClient.PostDataAsync(messageTag, dataItem, async response => { @@ -219,9 +219,9 @@ namespace TIAMSharedUI.Shared.Components.Grids return; } - _dataSource.UpdateCollection(response.ResponseData, dataChangeMode == DataChangeMode.Remove); + _dataSource.UpdateCollection(response.ResponseData, trackingState == TrackingState.Remove); - var changedEventArgs = new GridDataItemChangedEventArgs(this, response.ResponseData, dataChangeMode); + var changedEventArgs = new GridDataItemChangedEventArgs(this, response.ResponseData, trackingState); await OnDataItemChanged.InvokeAsync(changedEventArgs); InvokeAsync(StateHasChanged).Forget(); diff --git a/TIAMWebApp/Server/Controllers/ServiceProviderAPIController.cs b/TIAMWebApp/Server/Controllers/ServiceProviderAPIController.cs index bc7ee8b3..f1f8419d 100644 --- a/TIAMWebApp/Server/Controllers/ServiceProviderAPIController.cs +++ b/TIAMWebApp/Server/Controllers/ServiceProviderAPIController.cs @@ -30,9 +30,9 @@ namespace TIAMWebApp.Server.Controllers [NonAction] [ApiExplorerSettings(IgnoreApi = true)] - private async Task CompanyDataChanging(Company company, DataChangeMode dataChangeMode) + private async Task CompanyDataChanging(Company company, TrackingState trackingState) { - var logText = $"[{dataChangeMode.ToString().ToUpper()}] CompanyDataChanging called; Id: {company.Id}; OwnerId: {company.OwnerId}; Name: {company.Name}"; + var logText = $"[{trackingState.ToString().ToUpper()}] CompanyDataChanging called; Id: {company.Id}; OwnerId: {company.OwnerId}; Name: {company.Name}"; if (company.Name.IsNullOrEmpty()) { @@ -42,9 +42,9 @@ namespace TIAMWebApp.Server.Controllers _logger.Info(logText); - switch (dataChangeMode) + switch (trackingState) { - case DataChangeMode.Add: + case TrackingState.Add: if (company.Id.IsNullOrEmpty()) company.Id = Guid.NewGuid(); //if (company.OwnerId.IsNullOrEmpty()) company.OwnerId = Guid.Parse("540271F6-C604-4C16-8160-D5A7CAFEDF00"); //TESZT - J. @@ -54,13 +54,13 @@ namespace TIAMWebApp.Server.Controllers return await adminDal.CreateServiceProviderAsync(company); - case DataChangeMode.Update: + case TrackingState.Update: return await adminDal.UpdateCompanyAsync(company); - case DataChangeMode.Remove: + case TrackingState.Remove: return await adminDal.RemoveCompanyAsync(company); default: - throw new ArgumentOutOfRangeException(nameof(dataChangeMode), dataChangeMode, null); + throw new ArgumentOutOfRangeException(nameof(trackingState), trackingState, null); } } @@ -68,21 +68,21 @@ namespace TIAMWebApp.Server.Controllers [ApiExplorerSettings(IgnoreApi = true)] [SignalR(SignalRTags.AddCompany)] public async Task AddCompanyAsync(Company company) - => await CompanyDataChanging(company, DataChangeMode.Add) ? company.ToJson() : string.Empty; + => await CompanyDataChanging(company, TrackingState.Add) ? company.ToJson() : string.Empty; [AllowAnonymous] [HttpPost] [Route(APIUrls.UpdateServiceProviderRouteName)] [SignalR(SignalRTags.UpdateCompany)] public async Task UpdateServiceProvider(Company company) - => await CompanyDataChanging(company, DataChangeMode.Update) ? company.ToJson() : string.Empty; + => await CompanyDataChanging(company, TrackingState.Update) ? company.ToJson() : string.Empty; [AllowAnonymous] [HttpPost] [Route(APIUrls.RemoveServiceProviderRouteName)] [SignalR(SignalRTags.RemoveCompany)] public async Task RemoveServiceProvider(Company company) - => await CompanyDataChanging(company, DataChangeMode.Remove) ? company.ToJson() : string.Empty; + => await CompanyDataChanging(company, TrackingState.Remove) ? company.ToJson() : string.Empty; //15. [AllowAnonymous] @@ -220,9 +220,9 @@ namespace TIAMWebApp.Server.Controllers [NonAction] [ApiExplorerSettings(IgnoreApi = true)] - private async Task CarDataChanging(Car car, DataChangeMode dataChangeMode) + private async Task CarDataChanging(Car car, TrackingState trackingState) { - var logText = $"[{dataChangeMode.ToString().ToUpper()}] CarDataChanging called; Id: {car.Id}; OwnerId: {car.UserProductMappingId}; LicensePlate: {car.LicencePlate}"; + var logText = $"[{trackingState.ToString().ToUpper()}] CarDataChanging called; Id: {car.Id}; OwnerId: {car.UserProductMappingId}; LicensePlate: {car.LicencePlate}"; if (car.UserProductMappingId.IsNullOrEmpty() || car.LicencePlate.IsNullOrWhiteSpace()) { @@ -232,19 +232,19 @@ namespace TIAMWebApp.Server.Controllers _logger.Info(logText); - switch (dataChangeMode) + switch (trackingState) { - case DataChangeMode.Add: + case TrackingState.Add: if (car.Id.IsNullOrEmpty()) car.Id = Guid.NewGuid(); return await adminDal.AddCarAsync(car); - case DataChangeMode.Update: + case TrackingState.Update: return await adminDal.UpdateCarAsync(car); - case DataChangeMode.Remove: + case TrackingState.Remove: return await adminDal.RemoveCarAsync(car); default: - throw new ArgumentOutOfRangeException(nameof(dataChangeMode), dataChangeMode, null); + throw new ArgumentOutOfRangeException(nameof(trackingState), trackingState, null); } } @@ -255,7 +255,7 @@ namespace TIAMWebApp.Server.Controllers [EndpointSummary("Create car")] [SignalR(SignalRTags.CreateCar)] public async Task CreateCar(Car car) - => await CarDataChanging(car, DataChangeMode.Add) ? Ok(car) : BadRequest("Invalid request"); + => await CarDataChanging(car, TrackingState.Add) ? Ok(car) : BadRequest("Invalid request"); [AllowAnonymous] [HttpPost] @@ -264,7 +264,7 @@ namespace TIAMWebApp.Server.Controllers [EndpointSummary("Update car")] [SignalR(SignalRTags.UpdateCar)] public async Task UpdateCar(Car car) - => await CarDataChanging(car, DataChangeMode.Update) ? Ok(car) : BadRequest("Invalid request"); + => await CarDataChanging(car, TrackingState.Update) ? Ok(car) : BadRequest("Invalid request"); [AllowAnonymous] [HttpPost] @@ -273,7 +273,7 @@ namespace TIAMWebApp.Server.Controllers [EndpointSummary("Delete car")] [SignalR(SignalRTags.DeleteCar)] public async Task DeleteCar(Car car) - => await CarDataChanging(car, DataChangeMode.Remove) ? Ok(car) : BadRequest("Invalid request"); + => await CarDataChanging(car, TrackingState.Remove) ? Ok(car) : BadRequest("Invalid request"); [HttpPost] [Route(APIUrls.AddProductRouteName)] diff --git a/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs b/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs index b9200d75..d531c604 100644 --- a/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs +++ b/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs @@ -2,6 +2,7 @@ using AyCode.Core; using AyCode.Core.Extensions; using AyCode.Core.Helpers; +using AyCode.Core.Interfaces; using AyCode.Services.Loggers; using AyCode.Services.SignalRs; using MessagePack.Resolvers; @@ -75,9 +76,6 @@ namespace TIAMWebApp.Shared.Application.Services public virtual Task SendMessageToServerAsync(int messageTag) => SendMessageToServerAsync(messageTag, null, AcDomain.NextUniqueInt32); - public virtual Task SendMessageToServerAsync(int messageTag, int requestId) - => SendMessageToServerAsync(messageTag, null, requestId); - public virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId) { Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"); @@ -89,8 +87,8 @@ namespace TIAMWebApp.Shared.Application.Services } #region CRUD - public virtual Task GetByIdAsync(int messageTag, Guid id) where TResponse : class - => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(id)), AcDomain.NextUniqueInt32); + public virtual Task GetByIdAsync(int messageTag, Guid id) where TResponseData : class + => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(id)), AcDomain.NextUniqueInt32); public virtual Task GetByIdAsync(int messageTag, Guid id, Action> responseCallback) => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(id)), responseCallback); @@ -117,7 +115,7 @@ namespace TIAMWebApp.Shared.Application.Services public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message) where TResponse : class => SendMessageToServerAsync(messageTag, message, AcDomain.NextUniqueInt32); - public virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int requestId) where TResponse : class + protected virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int requestId) where TResponse : class { Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"); diff --git a/TIAMWebApp/Shared/Utility/SignalRDataSource.cs b/TIAMWebApp/Shared/Utility/SignalRDataSource.cs index 37780551..19b2c826 100644 --- a/TIAMWebApp/Shared/Utility/SignalRDataSource.cs +++ b/TIAMWebApp/Shared/Utility/SignalRDataSource.cs @@ -1,6 +1,10 @@ using System.Collections; +using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Formatters.Binary; using AyCode.Core.Enums; using AyCode.Core.Extensions; using AyCode.Core.Interfaces; @@ -10,22 +14,84 @@ using TIAMWebApp.Shared.Application.Services; namespace TIAMWebApp.Shared.Application.Utility { - public class ChangeTracking(DataChangeMode dataChangeMode, T newItem, T originalItem = default(T))where T: class, IId + public class TrackingItem(TrackingState trackingState, T currentValue, T? originalValue = null) where T : class, IId { - public DataChangeMode DataChangeMode { get; init; } = dataChangeMode; - public T NewItem { get; init; } = newItem; - public T OriginalItem { get; init; } = originalItem; + public TrackingState TrackingState { get; internal set; } = trackingState; + public T CurrentValue { get; internal set; } = currentValue; + public T? OriginalValue { get; init; } = originalValue; //originalValue == null ? null : TrackingItemHelpers.Clone(originalValue); + + internal TrackingItem UpdateItem(TrackingState trackingState, T newValue) + { + CurrentValue = newValue; + + if (TrackingState != TrackingState.Add) + TrackingState = trackingState; + + return this; + } } + + public class ChangeTracking where T : class, IId + { + private readonly List> _trackingItems = []; //TODO: Dictionary... - J. + + internal TrackingItem? AddTrackingItem(TrackingState trackingState, T newValue, T? originalValue = null) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), $@"currentValue.Id.IsNullOrEmpty()"); + + var itemIndex = _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.ReflectionClone(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(); + + public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) + { + trackingItem = _trackingItems.FirstOrDefault(x => x.CurrentValue.Id == id); + return trackingItem != null; + } + + internal void Remove(TrackingItem trackingItem) => _trackingItems.Remove(trackingItem); + } + + + [Serializable] [DebuggerDisplay("Count = {Count}")] - public class SignalRDataSource : IList, IList, IReadOnlyList where T: class, IId + public class SignalRDataSource : IList, IList, IReadOnlyList where T : class, IId { - protected readonly List InnerList = []; private readonly object _syncRoot = new(); - protected Guid? ContextId; - protected AcSignalRClientBase SignalRClient; + protected readonly List InnerList = []; //TODO: Dictionary??? - J. + protected readonly ChangeTracking TrackingItems = new(); + + protected readonly Guid? ContextId; + protected readonly AcSignalRClientBase SignalRClient; protected readonly SignalRCrudTags SignalRCrudTags; public SignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, Guid? contextId = null, bool autoLoadDataSource = true) @@ -35,7 +101,7 @@ namespace TIAMWebApp.Shared.Application.Utility SignalRCrudTags = signalRCrudTags; SignalRClient = signalRClient; - if (autoLoadDataSource) LoadDataSource(); + if (autoLoadDataSource) LoadDataSource(false); } public bool IsSynchronized => true; @@ -47,19 +113,38 @@ namespace TIAMWebApp.Shared.Application.Utility /// /// /// - public void LoadDataSource() + public void LoadDataSource(bool clearChangeTracking = true) { - if (SignalRCrudTags.GetAllMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.GetAllMessageTag == SignalRTags.None;"); + if (SignalRCrudTags.GetAllMessageTag == SignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); lock (_syncRoot) { var resultList = SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, ContextId).GetAwaiter().GetResult() ?? throw new NullReferenceException(); - Clear(); + Clear(clearChangeTracking); InnerList.AddRange(resultList); } } + public T? LoadItem(Guid id) + { + if (SignalRCrudTags.GetItemMessageTag == SignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None"); + + T? resultitem = null; + + lock (_syncRoot) + { + resultitem = SignalRClient.GetByIdAsync(SignalRCrudTags.GetItemMessageTag, id).GetAwaiter().GetResult(); + if (resultitem == null) return null; + + if (TryGetIndex(id, out var index)) InnerList[index] = resultitem; + else InnerList.Add(resultitem); + } + + return resultitem; + } + + /// /// set: UpdateMessageTag /// @@ -86,42 +171,38 @@ namespace TIAMWebApp.Shared.Application.Utility } } - public int Count - { - get - { - lock (_syncRoot) return InnerList.Count; - } - } - /// /// AddMessageTag /// - /// + /// /// - public void Add(T item) + public void Add(T newValue) { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Add->newValue.Id.IsNullOrEmpty()"); + lock (_syncRoot) { - if (Contains(item)) - throw new ArgumentException($@"It already contains this Id! Id: {item.Id}", nameof(item)); + if (Contains(newValue)) + throw new ArgumentException($@"It already contains this Id! Id: {newValue.Id}", nameof(newValue)); - UnsafeAdd(item); + UnsafeAdd(newValue); } } /// /// AddMessageTag or UpdateMessageTag /// - /// + /// /// - public T AddOrUpdate(T item) + public T AddOrUpdate(T newValue) { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()"); + lock (_syncRoot) { - var index = IndexOf(item); + var index = IndexOf(newValue); - return index > -1 ? Update(index, item) : UnsafeAdd(item); + return index > -1 ? Update(index, newValue) : UnsafeAdd(newValue); } } @@ -133,69 +214,67 @@ namespace TIAMWebApp.Shared.Application.Utility // } //} - private T UnsafeAdd(T item) + protected T UnsafeAdd(T newValue) { - if (SignalRCrudTags.AddMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.AddMessageTag == SignalRTags.None;"); + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Add(newValue); - var result = SignalRClient.PostDataAsync(SignalRCrudTags.AddMessageTag, item).GetAwaiter().GetResult() ?? throw new NullReferenceException(); - InnerList.Add(result); - - return result; + return newValue; } /// /// AddMessageTag /// /// - /// + /// /// /// - public void Insert(int index, T item) + public void Insert(int index, T newValue) { - if (SignalRCrudTags.AddMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.AddMessageTag == SignalRTags.None;"); + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); lock (_syncRoot) { - if (Contains(item)) - throw new ArgumentException($@"It already contains this Id! Id: {item.Id}", nameof(item)); + if (Contains(newValue)) + throw new ArgumentException($@"It already contains this Id! Id: {newValue.Id}", nameof(newValue)); - var result = SignalRClient.PostDataAsync(SignalRCrudTags.AddMessageTag, item).GetAwaiter().GetResult() ?? throw new NullReferenceException(); - InnerList.Insert(index, result); + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Insert(index, newValue); } } /// /// UpdateMessageTag /// - /// - public T Update(T item) => Update(IndexOf(item), item); + /// + public T Update(T newItem) => Update(IndexOf(newItem), newItem); /// /// UpdateMessageTag /// /// - /// + /// /// /// /// /// /// /// - public T Update(int index, T item) + public T Update(int index, T newValue) { - if (SignalRCrudTags.UpdateMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.UpdateMessageTag == SignalRTags.None;"); - - if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); - if (item.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(item), "Update->item.Id.IsNullOrEmpty()"); + if (default(T) != null && newValue == null) throw new NullReferenceException(nameof(newValue)); + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Update->newValue.Id.IsNullOrEmpty()"); if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); lock (_syncRoot) { - if (InnerList[index].Id != item.Id) - throw new ArgumentException($@"_list[index].Id != item.Id! Id: {item.Id}", nameof(item)); + var currentItem = InnerList[index]; - var result = SignalRClient.PostDataAsync(SignalRCrudTags.UpdateMessageTag, item).GetAwaiter().GetResult() ?? throw new NullReferenceException(); - - InnerList[index] = result; - return result; + if (currentItem.Id != newValue.Id) + throw new ArgumentException($@"currentItem.Id != item.Id! Id: {newValue.Id}", nameof(newValue)); + + TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem); + InnerList[index] = newValue; + + return newValue; } } @@ -217,6 +296,20 @@ namespace TIAMWebApp.Shared.Application.Utility } } + /// + /// + /// + /// + /// + /// + public bool TryRemove(Guid id, out T? item) + { + lock (_syncRoot) + { + return TryGetValue(id, out item) && Remove(item); + } + } + /// /// RemoveMessageTag /// @@ -226,34 +319,197 @@ namespace TIAMWebApp.Shared.Application.Utility /// public void RemoveAt(int index) { - if (SignalRCrudTags.RemoveMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.RemoveMessageTag == SignalRTags.None;"); - lock (_syncRoot) { - var item = InnerList[index]; - if (item.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(item), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}"); - - var result = SignalRClient.PostDataAsync(SignalRCrudTags.RemoveMessageTag, item).GetAwaiter().GetResult() ?? throw new NullReferenceException(); + var currentItem = InnerList[index]; + if (currentItem.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(currentItem), $@"RemoveAt->item.Id.IsNullOrEmpty(); index: {index}"); + TrackingItems.AddTrackingItem(TrackingState.Remove, currentItem, currentItem); InnerList.RemoveAt(index); } } - public void Clear() - { - lock (_syncRoot) InnerList.Clear(); - } - - public int IndexOf(T item) + /// + /// + /// + /// + public List> GetTrackingItems() { lock (_syncRoot) - return InnerList.FindIndex(x => x.Id == item.Id); + return TrackingItems.ToList(); } - public bool Contains(T item) + public void SetTrackingStateToUpdate(T item) { - lock (_syncRoot) - return IndexOf(item) > -1; + if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem)) + { + if (trackingItem.TrackingState != TrackingState.Add) + trackingItem.TrackingState = TrackingState.Update; + + return; + } + + TrackingItems.AddTrackingItem(TrackingState.Update, item, item); + } + + /// + /// + /// + /// + /// + /// + public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) + { + lock (_syncRoot) + return TrackingItems.TryGetTrackingItem(id, out trackingItem); + } + + /// + /// + /// + /// Unsaved items + public bool SaveChanges(out List> unsavedItems) + { + lock (_syncRoot) + { + foreach (var trackingItem in TrackingItems.ToList()) + { + try + { + SaveTrackingItemUnsafe(trackingItem); + } + catch + { + // ignored + } + } + + unsavedItems = TrackingItems.ToList(); + return unsavedItems.Count == 0; + } + } + + /// + /// + /// + /// + /// + /// + public bool TrySaveItem(Guid id, [NotNullWhen(true)] out T? resultItem) + { + resultItem = null; + + if (TryGetTrackingItem(id, out var trackingItem)) + resultItem = SaveTrackingItemUnsafe(trackingItem); + + return resultItem != null; + } + + public bool TrySaveItem(Guid id, TrackingState trackingState, [NotNullWhen(true)] out T? resultItem) + => TryGetValue(id, out resultItem) && TrySaveItem(resultItem, trackingState, out resultItem); + + public bool TrySaveItem(T item, TrackingState trackingState, [NotNullWhen(true)] out T? resultItem) + { + resultItem = SaveItemUnsafe(item, trackingState); + return resultItem != null; + } + + protected T? SaveTrackingItemUnsafe(TrackingItem trackingItem) + => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState); + + protected T? SaveItemUnsafe(T item, TrackingState trackingState) + { + var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); + if (messageTag == SignalRTags.None) return null; //throw new ArgumentException($"messageTag == SignalRTags.None"); + + var result = SignalRClient.PostDataAsync(messageTag, item).GetAwaiter().GetResult(); + if (result == null) return null; //throw new NullReferenceException($"result == null"); + + if (TryGetTrackingItem(item.Id, out var trackingItem)) + TrackingItems.Remove(trackingItem); + + if (TryGetIndex(result.Id, out var index)) + InnerList[index] = result; + + return result; + } + + protected void RollbackItemUnsafe(TrackingItem trackingItem) + { + if (TryGetIndex(trackingItem.CurrentValue.Id, out var index)) + { + if (trackingItem.TrackingState == TrackingState.Add) InnerList.RemoveAt(index); + else InnerList[index] = trackingItem.OriginalValue!; + } + else if (trackingItem.TrackingState != TrackingState.Add) + InnerList.Add(trackingItem.OriginalValue!); + + TrackingItems.Remove(trackingItem); + } + + public bool TryRollbackItem(Guid id, out T? originalValue) + { + lock (_syncRoot) + { + if (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); + } + } + + public int Count + { + get + { + lock (_syncRoot) return InnerList.Count; + } + } + + public void Clear() => Clear(true); + + public void Clear(bool clearChangeTracking) + { + lock (_syncRoot) + { + if (clearChangeTracking) TrackingItems.Clear(); + InnerList.Clear(); + } + } + + public int IndexOf(Guid id) + { + lock (_syncRoot) + return InnerList.FindIndex(x => x.Id == id); + } + + public int IndexOf(T item) => IndexOf(item.Id); + public bool TryGetIndex(Guid id, out int index) => (index = IndexOf(id)) > -1; + + public bool Contains(T item) => IndexOf(item) > -1; + + public bool TryGetValue(Guid id, [NotNullWhen(true)] out T? item) + { + lock (_syncRoot) + { + item = InnerList.FirstOrDefault(x => x.Id == id); + return item != null; + } } public void CopyTo(T[] array) => CopyTo(array, 0); @@ -290,6 +546,7 @@ namespace TIAMWebApp.Shared.Application.Utility #region IList, ICollection + bool IList.IsReadOnly => false; object? IList.this[int index] @@ -326,11 +583,11 @@ namespace TIAMWebApp.Shared.Application.Utility return Count - 1; } - void IList.Clear() => Clear(); + void IList.Clear() => Clear(true); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); bool IList.Contains(object? item) => IsCompatibleObject(item) && Contains((T)item!); int IList.IndexOf(object? item) => (IsCompatibleObject(item)) ? IndexOf((T)item!) : -1; - + void IList.Insert(int index, object? item) { if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); @@ -350,7 +607,7 @@ namespace TIAMWebApp.Shared.Application.Utility if (IsCompatibleObject(item)) Remove((T)item!); } - void ICollection.Clear() => Clear(); + void ICollection.Clear() => Clear(true); void ICollection.CopyTo(Array array, int arrayIndex) { @@ -361,8 +618,11 @@ namespace TIAMWebApp.Shared.Application.Utility try { - //TODO: _list.ToArray() - ez nem az igazi... - J. - Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); + lock (_syncRoot) + { + //TODO: _list.ToArray() - ez nem az igazi... - J. + Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); + } } catch (ArrayTypeMismatchException) { @@ -375,6 +635,7 @@ namespace TIAMWebApp.Shared.Application.Utility bool ICollection.IsReadOnly => false; void IList.RemoveAt(int index) => RemoveAt(index); int IReadOnlyCollection.Count => Count; + #endregion IList, ICollection } } diff --git a/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs b/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs index f46ff930..e6361fa2 100644 --- a/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs +++ b/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs @@ -23,7 +23,7 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I if (autoLoadDataSource) LoadDataSourceAsync(); } - public void LoadDataSourceAsync() + public void LoadDataSourceAsync(bool clearChangeTracking = true) { if (SignalRCrudTags.GetAllMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.GetAllMessageTag == SignalRTags.None;"); @@ -39,7 +39,7 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I if (response.Status == SignalResponseStatus.Error) throw new Exception($"LoadDataSourceAsync; response.Status == SignalResponseStatus.Error"); if (response.ResponseData == null) throw new NullReferenceException($"response.ResponseData == null"); - Clear(); + Clear(clearChangeTracking); InnerList.AddRange(response.ResponseData); } finally @@ -58,13 +58,13 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I } - //public T Add(T item, int messageTag) => PostDataToServerAsync(item, messageTag, DataChangeMode.Add).GetAwaiter().GetResult(); - //public Task AddAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, DataChangeMode.Add); + //public T Add(T item, int messageTag) => PostDataToServerAsync(item, messageTag, TrackingState.Add).GetAwaiter().GetResult(); + //public Task AddAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, TrackingState.Add); - //public Task UpdateAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, DataChangeMode.Update); + //public Task UpdateAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, TrackingState.Update); - //public Task RemoveAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, DataChangeMode.Remove); + //public Task RemoveAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, TrackingState.Remove); //public Task RemoveAsync(Guid id, int messageTag) //{ @@ -73,7 +73,7 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I // return item == null ? Task.CompletedTask : RemoveAsync(item, messageTag); //} - //protected virtual Task PostDataToServerAsync(T item, int messageTag, DataChangeMode dataChangeMode) + //protected virtual Task PostDataToServerAsync(T item, int messageTag, TrackingState trackingState) //{ // if (messageTag == 0) return Task.CompletedTask; @@ -81,7 +81,7 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I // if (item.Id.IsNullOrEmpty()) item.Id = Guid.NewGuid(); - // _list.UpdateCollection(item, dataChangeMode == DataChangeMode.Remove); //egyből látszódik a változás a grid-ben, nem csak a callback lefutásakor! felhasználóbarátabb... - J. + // _list.UpdateCollection(item, trackingState == TrackingState.Remove); //egyből látszódik a változás a grid-ben, nem csak a callback lefutásakor! felhasználóbarátabb... - J. // await _signalRClient.PostDataAsync(messageTag, item, async repsonse => // { @@ -91,9 +91,9 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I // return; // } - // _list.UpdateCollection(repsonse.ResponseData, dataChangeMode == DataChangeMode.Remove); + // _list.UpdateCollection(repsonse.ResponseData, trackingState == TrackingState.Remove); - // var eventArgs = new ItemChangedEventArgs(repsonse.ResponseData, dataChangeMode); + // var eventArgs = new ItemChangedEventArgs(repsonse.ResponseData, trackingState); // OnItemChanged.Invoke(eventArgs); // }); @@ -104,14 +104,14 @@ public class SignalRDataSourceAsync : SignalRDataSource where T : class, I public class ItemChangedEventArgs where T : IId { - internal ItemChangedEventArgs(T item, DataChangeMode dataChangeMode) + internal ItemChangedEventArgs(T item, TrackingState trackingState) { Item = item; - DataChangeMode = dataChangeMode; + TrackingState = trackingState; } public T Item { get; } - public DataChangeMode DataChangeMode { get; } + public TrackingState TrackingState { get; } } } \ No newline at end of file diff --git a/TIAMWebApp/Shared/Utility/TrackingItemHelpers.cs b/TIAMWebApp/Shared/Utility/TrackingItemHelpers.cs new file mode 100644 index 00000000..34b5eadc --- /dev/null +++ b/TIAMWebApp/Shared/Utility/TrackingItemHelpers.cs @@ -0,0 +1,47 @@ +using System.Reflection; +using System.Text.Json; +using AyCode.Core.Extensions; + +namespace TIAMWebApp.Shared.Application.Utility; + +public static class TrackingItemHelpers +{ + public static T JsonClone(T source) => source.ToJson().JsonTo()!; + + public static T ReflectionClone(T source) + { + var type = source!.GetType(); + + if (type.IsPrimitive || typeof(string) == type) + return source; + + if (type.IsArray) + { + var elementType = Type.GetType(type.FullName!.Replace("[]", string.Empty))!; + var array = (source as Array)!; + var cloned = Array.CreateInstance(elementType, array.Length); + + for (var i = 0; i < array.Length; i++) + cloned.SetValue(ReflectionClone(array.GetValue(i)), i); + + return (T)Convert.ChangeType(cloned, type); + } + + var clone = Activator.CreateInstance(type); + + while (type != null && type != typeof(object)) + { + foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) + { + var fieldValue = field.GetValue(source); + if (fieldValue == null) continue; + + field.SetValue(clone, ReflectionClone(fieldValue)); + } + + type = type.BaseType; + } + + return (T)clone!; + } +} \ No newline at end of file