From 50869e47542d78336d76203abd707c17d71e12da Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 21 Jun 2024 14:32:04 +0200 Subject: [PATCH] multiple signalr param; improvements, fixes, etc.. --- .../AyCode.Blazor.Components.csproj | 3 + .../Components/ACComponent.cs | 4 +- .../Services/AcGridDataSource.cs | 78 ++ .../Services/AcSignalRClientBase.cs | 247 ++++ .../Services/AcSignalRDataSource.cs | 1029 +++++++++++++++++ .../Services/TrackingItemHelpers.cs | 46 + .../AyCode.Blazor.Controllers.csproj | 6 + AyCode.Maui.Core/AyCode.Maui.Core.csproj | 6 + 8 files changed, 1417 insertions(+), 2 deletions(-) create mode 100644 AyCode.Blazor.Components/Services/AcGridDataSource.cs create mode 100644 AyCode.Blazor.Components/Services/AcSignalRClientBase.cs create mode 100644 AyCode.Blazor.Components/Services/AcSignalRDataSource.cs create mode 100644 AyCode.Blazor.Components/Services/TrackingItemHelpers.cs diff --git a/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj b/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj index fcc2eb4..df52bcc 100644 --- a/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj +++ b/AyCode.Blazor.Components/AyCode.Blazor.Components.csproj @@ -14,7 +14,10 @@ + + + diff --git a/AyCode.Blazor.Components/Components/ACComponent.cs b/AyCode.Blazor.Components/Components/ACComponent.cs index 08cc273..26aef27 100644 --- a/AyCode.Blazor.Components/Components/ACComponent.cs +++ b/AyCode.Blazor.Components/Components/ACComponent.cs @@ -1,8 +1,8 @@ using Microsoft.AspNetCore.Components; -namespace AyCode.Blazor.Components +namespace AyCode.Blazor.Components.Components { - public class ACComponent : ComponentBase + public class AcComponent : ComponentBase { } } diff --git a/AyCode.Blazor.Components/Services/AcGridDataSource.cs b/AyCode.Blazor.Components/Services/AcGridDataSource.cs new file mode 100644 index 0000000..1b87431 --- /dev/null +++ b/AyCode.Blazor.Components/Services/AcGridDataSource.cs @@ -0,0 +1,78 @@ +using DevExpress.Blazor; +using DevExpress.Data.Filtering; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AyCode.Core.Interfaces; +using DevExpress.Data.Filtering.Helpers; +using DevExpress.Data.Linq; +using DevExpress.Data.Linq.Helpers; + +namespace AyCode.Blazor.Components.Services +{ + //public class AcGridDataSource : GridCustomDataSource where T : class, IId + //{ + // private static readonly AcSignalRDataSource _signalRDataSource; + + // public AcGridDataSource(AcSignalRDataSource signalRDataSource) + // { + // _signalRDataSource = signalRDataSource; + // } + + // public override async Task GetItemCountAsync(GridCustomDataSourceCountOptions options, CancellationToken cancellationToken) + // { + // return await ApplyFiltering(options.FilterCriteria, _signalRDataSource.For()).Count().FindScalarAsync(cancellationToken); + // } + + // public override async Task GetItemsAsync(GridCustomDataSourceItemsOptions options, CancellationToken cancellationToken) + // { + // var filteredClient = ApplyFiltering(options.FilterCriteria, _signalRDataSource.For().Top(options.Count).Skip(options.StartIndex)); + // return (await ApplySorting(options, filteredClient).FindEntriesAsync(cancellationToken)).ToList(); + // } + + // private static IBoundClient ApplyFiltering(CriteriaOperator criteria, IBoundClient boundClient) + // { + // //return !criteria.ReferenceEqualsNull() ? boundClient.Filter(ToSimpleClientCriteria(criteria)) : boundClient; + + // CriteriaToExpressionConverter converter = new CriteriaToExpressionConverter(); + // //IQueryable source = null!; + // IQueryable? filteredData = _signalRDataSource.AsQueryable().AppendWhere(converter, criteria) as IQueryable; + + // gridControl1.DataSource = null; + // gridControl1.DataSource = filteredData.ToList(); + // } + + // private static string ToSimpleClientCriteria(CriteriaOperator criteria) + // => $"{criteria}".Replace("[", "").Replace("]", ""); + + // private static IBoundClient ApplySorting(GridCustomDataSourceItemsOptions options, IBoundClient boundClient) + // { + // return options.SortInfo.Any() + // ? boundClient.OrderBy(options.SortInfo + // .Where(info => !info.DescendingSortOrder).Select(info => info.FieldName).ToArray()) + // .OrderByDescending(options.SortInfo + // .Where(info => info.DescendingSortOrder).Select(info => info.FieldName).ToArray()) + // : boundClient; + // } + + // public async Task DeleteAsync(T instance) + // => await _signalRDataSource.For().Key(instance.Id).DeleteEntryAsync(); + + // public async Task AddOrUpdateAsync(T instance, bool update = false) + // { + // if (!update) + // { + // await _signalRDataSource.For().Set(new { instance.Title, instance.Content }).InsertEntryAsync(); + // } + // else + // { + + // await _signalRDataSource.For().Key(instance.Id).Set(instance).UpdateEntryAsync(); + // } + // } + //} +} + diff --git a/AyCode.Blazor.Components/Services/AcSignalRClientBase.cs b/AyCode.Blazor.Components/Services/AcSignalRClientBase.cs new file mode 100644 index 0000000..c084341 --- /dev/null +++ b/AyCode.Blazor.Components/Services/AcSignalRClientBase.cs @@ -0,0 +1,247 @@ +using System.Collections.Concurrent; +using AyCode.Core; +using AyCode.Core.Extensions; +using AyCode.Core.Helpers; +using AyCode.Core.Loggers; +using AyCode.Services.Loggers; +using AyCode.Services.SignalRs; +using MessagePack.Resolvers; +using Microsoft.AspNetCore.SignalR.Client; + +namespace AyCode.Blazor.Components.Services +{ + public abstract class AcSignalRClientBase : IAcSignalRHubClient + { + private readonly ConcurrentDictionary _responseByRequestId = new(); + + protected readonly HubConnection HubConnection; + protected readonly AcLoggerBase Logger; + + public event Action OnMessageReceived = null!; + //public event Action OnMessageRequested; + + public int Timeout = 10000; + + protected AcSignalRClientBase(string fullHubName, AcLoggerBase logger) + { + Logger = logger; + + HubConnection = new HubConnectionBuilder() + .WithUrl(fullHubName) + //.AddMessagePackProtocol(options => { + // options.SerializerOptions = MessagePackSerializerOptions.Standard + // .WithResolver(MessagePack.Resolvers.StandardResolver.Instance) + // .WithSecurity(MessagePackSecurity.UntrustedData) + // .WithCompression(MessagePackCompression.Lz4Block) + // .WithCompressionMinLength(256);}) + .Build(); + + HubConnection.Closed += HubConnection_Closed; + + _ = HubConnection.On(nameof(IAcSignalRHubClient.OnReceiveMessage), OnReceiveMessage); + //_ = HubConnection.On(nameof(IAcSignalRHubClient.OnRequestMessage), OnRequestMessage); + + HubConnection.StartAsync().Forget(); + + } + + private Task HubConnection_Closed(Exception? arg) + { + if (_responseByRequestId.IsEmpty) Logger.DebugConditional($"Client HubConnection_Closed"); + else Logger.Warning($"Client HubConnection_Closed; {nameof(_responseByRequestId)} count: {_responseByRequestId.Count}"); + + _responseByRequestId.Clear(); + return Task.CompletedTask; + } + + public async Task StartConnection() + { + if (HubConnection.State == HubConnectionState.Disconnected) + await HubConnection.StartAsync(); + + if (HubConnection.State != HubConnectionState.Connected) + await TaskHelper.WaitToAsync(() => HubConnection.State == HubConnectionState.Connected, Timeout, 25); + } + + public async Task StopConnection() + { + await HubConnection.StopAsync(); + await HubConnection.DisposeAsync(); + } + + public virtual Task SendMessageToServerAsync(int messageTag) + => SendMessageToServerAsync(messageTag, null, AcDomain.NextUniqueInt32); + + public virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int? requestId) + { + Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"); + + await StartConnection(); + + var msgp = message?.ToMessagePack(ContractlessStandardResolver.Options); + HubConnection.SendAsync(nameof(IAcSignalRHubClient.OnReceiveMessage), messageTag, msgp, requestId).Forget(); + } + + #region CRUD + public virtual Task GetByIdAsync(int messageTag, params Guid[] ids) where TResponseData : class + => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(ids)), AcDomain.NextUniqueInt32); + public virtual Task GetByIdAsync(int messageTag, Action> responseCallback, params Guid[] ids) + => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(new IdMessage(ids)), responseCallback); + + public virtual Task GetAllAsync(int messageTag) where TResponseData : class + => SendMessageToServerAsync(messageTag); + public virtual Task GetAllAsync(int messageTag, Action> responseCallback) + => SendMessageToServerAsync(messageTag, null, responseCallback); + public virtual Task GetAllAsync(int messageTag, Action> responseCallback, params Guid[]? contextIds) + => SendMessageToServerAsync(messageTag, (contextIds == null || contextIds.Length == 0 ? null : new SignalPostJsonDataMessage(new IdMessage(contextIds))), responseCallback); + + public virtual Task GetAllAsync(int messageTag, params Guid[]? contextIds) where TResponseData : class + => SendMessageToServerAsync(messageTag, contextIds == null || contextIds.Length == 0 ? null : new SignalPostJsonDataMessage(new IdMessage(contextIds)), AcDomain.NextUniqueInt32); + + public virtual Task PostDataAsync(int messageTag, TPostData postData) where TPostData : class + => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), AcDomain.NextUniqueInt32); + public virtual Task PostDataAsync(int messageTag, TPostData postData, Action> responseCallback) where TPostData : class + => SendMessageToServerAsync(messageTag, new SignalPostJsonDataMessage(postData), responseCallback); + + #endregion CRUD + + public virtual Task SendMessageToServerAsync(int messageTag) where TResponse : class + => SendMessageToServerAsync(messageTag, null, AcDomain.NextUniqueInt32); + + public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message) where TResponse : class + => SendMessageToServerAsync(messageTag, message, AcDomain.NextUniqueInt32); + + protected virtual async Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, int requestId) where TResponse : class + { + Logger.DebugConditional($"Client SendMessageToServerAsync; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"); + + _responseByRequestId[requestId] = null; + await SendMessageToServerAsync(messageTag, message, requestId); + + try + { + if (await TaskHelper.WaitToAsync(() => _responseByRequestId[requestId] != null, Timeout, 25) && + _responseByRequestId.TryRemove(requestId, out var obj) && obj is ISignalResponseMessage responseMessage) + { + if (responseMessage.Status == SignalResponseStatus.Error || responseMessage.ResponseData == null) + { + var errorText = $"Client SendMessageToServerAsync response error; await; tag: {messageTag}; Status: {responseMessage.Status}; requestId: {requestId};"; + + Logger.Error(errorText); + + //TODO: Ideiglenes, majd a ResponseMessage-et kell visszaadni a Status miatt! - J. + throw new Exception(errorText); + //return default; + } + + return responseMessage.ResponseData.JsonTo(); + } + } + catch (Exception ex) + { + Logger.Error($"SendMessageToServerAsync; messageTag: {messageTag}; requestId: {requestId}; {ex.Message}", ex); + } + + _responseByRequestId.TryRemove(requestId, out _); + return default; + } + + public virtual Task SendMessageToServerAsync(int messageTag, Action> responseCallback) + => SendMessageToServerAsync(messageTag, null, responseCallback); + + public virtual Task SendMessageToServerAsync(int messageTag, ISignalRMessage? message, Action> responseCallback) + { + if (messageTag == 0) + Logger.Error($"SendMessageToServerAsync; messageTag == 0"); + + var requestId = AcDomain.NextUniqueInt32; + + _responseByRequestId[requestId] = new Action>(responseMessage => + { + TResponseData? responseData = default; + + if (responseMessage.Status == SignalResponseStatus.Success) + { + responseData = string.IsNullOrEmpty(responseMessage.ResponseData) ? default : responseMessage.ResponseData.JsonTo(); + } + else Logger.Error($"Client SendMessageToServerAsync response error; callback; tag: {messageTag}; Status: {responseMessage.Status}; requestId: {requestId};"); + + responseCallback(new SignalResponseMessage(responseMessage.Status, responseData)); + }); + + return SendMessageToServerAsync(messageTag, message, requestId); + } + + public virtual Task OnReceiveMessage(int messageTag, byte[] message, int? requestId) + { + var logText = $"Client OnReceiveMessage; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"; + + if (message.Length == 0) Logger.Warning($"message.Length == 0! {logText}"); + else Logger.Info(logText); + + try + { + if (requestId.HasValue && _responseByRequestId.ContainsKey(requestId.Value)) + { + var reqId = requestId.Value; + + var responseMessage = message.MessagePackTo(ContractlessStandardResolver.Options); + + switch (_responseByRequestId[reqId]) + { + case null: + _responseByRequestId[reqId] = responseMessage; + return Task.CompletedTask; + + case Action> messagePackCallback: + _responseByRequestId.TryRemove(reqId, out _); + + messagePackCallback.Invoke(responseMessage); + return Task.CompletedTask; + + //case Action jsonCallback: + // _responseByRequestId.TryRemove(reqId, out _); + + // jsonCallback.Invoke(responseMessage); + // return Task.CompletedTask; + + default: + Logger.Error($"Client OnReceiveMessage switch; unknown message type: {_responseByRequestId[reqId]?.GetType().Name}"); + break; + } + + _responseByRequestId.TryRemove(reqId, out _); + } + + OnMessageReceived(messageTag, message, requestId); + } + catch (Exception ex) + { + if (requestId.HasValue) + _responseByRequestId.TryRemove(requestId.Value, out _); + + Logger.Error($"Client OnReceiveMessage; messageTag: {messageTag}; requestId: {requestId}; {ex.Message}", ex); + throw; + } + + return Task.CompletedTask; + } + //public virtual Task OnRequestMessage(int messageTag, int requestId) + //{ + // Logger.DebugConditional($"Client OnRequestMessage; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId};"); + + // try + // { + // OnMessageRequested(messageTag, requestId); + // } + // catch(Exception ex) + // { + // Logger.Error($"Client OnReceiveMessage; {nameof(messageTag)}: {messageTag}; {nameof(requestId)}: {requestId}; {ex.Message}", ex); + // throw; + // } + + // return Task.CompletedTask; + + //} + } +} diff --git a/AyCode.Blazor.Components/Services/AcSignalRDataSource.cs b/AyCode.Blazor.Components/Services/AcSignalRDataSource.cs new file mode 100644 index 0000000..819765f --- /dev/null +++ b/AyCode.Blazor.Components/Services/AcSignalRDataSource.cs @@ -0,0 +1,1029 @@ +using System.Collections; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using AyCode.Core.Enums; +using AyCode.Core.Extensions; +using AyCode.Core.Helpers; +using AyCode.Core.Interfaces; +using AyCode.Services.SignalRs; + +namespace AyCode.Blazor.Components.Services +{ + public class TrackingItem(TrackingState trackingState, T currentValue, T? originalValue = null) where T : class, IId + { + public TrackingState TrackingState { get; internal set; } = trackingState; + public T CurrentValue { get; internal set; } = currentValue; + public T? OriginalValue { get; init; } = originalValue; + + internal TrackingItem UpdateItem(TrackingState trackingState, T newValue) + { + CurrentValue = newValue; + + if (TrackingState != TrackingState.Add) + TrackingState = trackingState; + + return this; + } + } + + + public class ChangeTracking /*: IEnumerable>*/ 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.JsonClone(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); + + //public IEnumerator> GetEnumerator() + //{ + // return _trackingItems.GetEnumerator(); + //} + + //IEnumerator IEnumerable.GetEnumerator() + //{ + // return GetEnumerator(); + //} + } + + + + [Serializable] + [DebuggerDisplay("Count = {Count}")] + public class AcSignalRDataSource : IList, IList, IReadOnlyList where T : class, IId + { + private readonly object _syncRoot = new(); + + protected readonly List InnerList = []; //TODO: Dictionary??? - J. + protected readonly ChangeTracking TrackingItems = new(); + + protected readonly Guid[]? ContextIds; + public AcSignalRClientBase SignalRClient; + protected readonly SignalRCrudTags SignalRCrudTags; + + public Func, Task>? OnDataSourceItemChanged; + public Func? OnDataSourceLoaded; + + public AcSignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, params Guid[]? contextIds) + { + if (contextIds?.Length > 0) ContextIds = contextIds; + + SignalRCrudTags = signalRCrudTags; + SignalRClient = signalRClient; + } + + public bool IsSynchronized => true; + public object SyncRoot => _syncRoot; + public bool IsFixedSize => false; + + /// + /// GetAllMessageTag + /// + /// + /// + public async Task LoadDataSource(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + var resultList = (await SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, ContextIds)) ?? throw new NullReferenceException(); + + await LoadDataSource(resultList); + } + + public Task LoadDataSourceAsync(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + return SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, result=> + { + if (result.Status != SignalResponseStatus.Success || result.ResponseData == null) + throw new NullReferenceException($"LoadDataSourceAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); + + LoadDataSource(result.ResponseData).Forget(); + }, ContextIds); + } + + public async Task LoadDataSource(IList fromSource, bool clearChangeTracking = true) + { + Monitor.Enter(_syncRoot); + + try + { + Clear(clearChangeTracking); + + foreach (var item in fromSource) + { + InnerList.Add(item); + + var eventArgs = new ItemChangedEventArgs(item, TrackingState.GetAll); + if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); + } + } + finally + { + Monitor.Exit(_syncRoot); + } + + if (OnDataSourceLoaded != null) await OnDataSourceLoaded.Invoke(); + } + + public async Task LoadItem(Guid id) + { + if (SignalRCrudTags.GetItemMessageTag == AcSignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetItemMessageTag == SignalRTags.None"); + + T? resultitem = null; + + Monitor.Enter(_syncRoot); + + try + { + resultitem = await SignalRClient.GetByIdAsync(SignalRCrudTags.GetItemMessageTag, id); + if (resultitem == null) return null; + + if (TryGetIndex(id, out var index)) InnerList[index] = resultitem; + else InnerList.Add(resultitem); + + var eventArgs = new ItemChangedEventArgs(resultitem, TrackingState.Get); + if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); + } + finally + { + Monitor.Exit(_syncRoot); + } + + return resultitem; + } + + /// + /// set: UpdateMessageTag + /// + /// + /// + /// + public T this[int index] + { + get + { + if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); + + Monitor.Enter(_syncRoot); + try + { + return InnerList[index]; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + set + { + Monitor.Enter(_syncRoot); + try + { + UpdateUnsafe(index, value); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + } + + public void Add(T newValue) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Add->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + if (Contains(newValue)) + throw new ArgumentException($@"It already contains this Id! Id: {newValue.Id}", nameof(newValue)); + + UnsafeAdd(newValue); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// AddMessageTag + /// + /// + /// + /// + public async Task Add(T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + Add(newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// AddMessageTag or UpdateMessageTag + /// + /// + /// + /// + public async Task AddOrUpdate(T newValue, bool autoSave) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"AddOrUpdate->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + var index = IndexOf(newValue); + + return index > -1 ? await Update(index, newValue, autoSave) : await Add(newValue, autoSave); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + //public void AddRange(IEnumerable collection) + //{ + // lock (_syncRoot) + // { + + // } + //} + + protected void UnsafeAdd(T newValue) + { + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Add(newValue); + } + + /// + /// AddMessageTag + /// + /// + /// + /// + /// + /// + public void Insert(int index, T newValue) + { + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"Insert->newValue.Id.IsNullOrEmpty()"); + + Monitor.Enter(_syncRoot); + + try + { + if (Contains(newValue)) + throw new ArgumentException($@"Insert; It already contains this Id! Id: {newValue.Id}", nameof(newValue)); + + TrackingItems.AddTrackingItem(TrackingState.Add, newValue); + InnerList.Insert(index, newValue); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task Insert(int index, T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + Insert(index, newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Add) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// UpdateMessageTag + /// + /// + /// + public Task Update(T newItem, bool autoSave) => Update(IndexOf(newItem), newItem, autoSave); + + /// + /// UpdateMessageTag + /// + /// + /// + /// + /// /// + /// /// + /// + /// + public async Task Update(int index, T newValue, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + UpdateUnsafe(index, newValue); + + return autoSave ? await SaveItem(newValue, TrackingState.Update) : newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + private void UpdateUnsafe(int index, T newValue) + { + if (default(T) != null && newValue == null) throw new NullReferenceException(nameof(newValue)); + if (newValue.Id.IsNullOrEmpty()) throw new ArgumentNullException(nameof(newValue), @"UpdateUnsafe->newValue.Id.IsNullOrEmpty()"); + if ((uint)index >= (uint)Count) throw new ArgumentOutOfRangeException(nameof(index)); + + Monitor.Enter(_syncRoot); + + try + { + var currentItem = InnerList[index]; + + if (currentItem.Id != newValue.Id) + throw new ArgumentException($@"UpdateUnsafe; currentItem.Id != item.Id! Id: {newValue.Id}", nameof(newValue)); + + TrackingItems.AddTrackingItem(TrackingState.Update, newValue, currentItem); + InnerList[index] = newValue; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + + /// + /// RemoveMessageTag + /// + /// + /// + public bool Remove(T item) + { + Monitor.Enter(_syncRoot); + + try + { + var index = IndexOf(item); + + if (index < 0) return false; + + RemoveAt(index); + return true; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task Remove(T item, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + var result = Remove(item); + + if (!autoSave || !result) return result; + + await SaveItem(item, TrackingState.Remove); + return true; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// + /// + /// + /// + /// + public bool TryRemove(Guid id, out T? item) + { + Monitor.Enter(_syncRoot); + + try + { + return TryGetValue(id, out item) && Remove(item); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + /// + /// RemoveMessageTag + /// + /// + /// + /// /// + /// + public void RemoveAt(int index) + { + Monitor.Enter(_syncRoot); + + try + { + 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); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task RemoveAt(int index, bool autoSave) + { + Monitor.Enter(_syncRoot); + + try + { + var currentItem = InnerList[index]; + RemoveAt(index); + + if (autoSave) + { + await SaveItem(currentItem, TrackingState.Remove); + } + } + finally + { + Monitor.Exit(_syncRoot); + } + } + /// + /// + /// + /// + public List> GetTrackingItems() + { + Monitor.Enter(_syncRoot); + + try + { + return TrackingItems.ToList(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void SetTrackingStateToUpdate(T item) + { + Monitor.Enter(_syncRoot); + + try + { + if (TrackingItems.TryGetTrackingItem(item.Id, out var trackingItem)) + { + if (trackingItem.TrackingState != TrackingState.Add) + trackingItem.TrackingState = TrackingState.Update; + + return; + } + + if (!TryGetValue(item.Id, out var originalItem)) return; + TrackingItems.AddTrackingItem(TrackingState.Update, item, originalItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// + /// + /// + /// + /// + public bool TryGetTrackingItem(Guid id, [NotNullWhen(true)] out TrackingItem? trackingItem) + { + Monitor.Enter(_syncRoot); + + try + { + return TrackingItems.TryGetTrackingItem(id, out trackingItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + /// + /// + /// + /// Unsaved items + public async Task>> SaveChanges() + { + Monitor.Enter(_syncRoot); + + try + { + foreach (var trackingItem in TrackingItems.ToList()) + { + try + { + await SaveTrackingItemUnsafe(trackingItem); + } + catch(Exception ex) + { + TryRollbackItem(trackingItem.CurrentValue.Id, out _); + } + } + + return TrackingItems.ToList(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task SaveChangesAsync() + { + Monitor.Enter(_syncRoot); + + try + { + foreach (var trackingItem in TrackingItems.ToList()) + { + try + { + await SaveTrackingItemUnsafeAsync(trackingItem); + } + catch(Exception ex) + { + TryRollbackItem(trackingItem.CurrentValue.Id, out _); + } + } + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + /// + /// + /// + /// + /// + public async Task SaveItem(Guid id) + { + Monitor.Enter(_syncRoot); + + try + { + T resultItem = null!; + + if (TryGetTrackingItem(id, out var trackingItem)) + resultItem = await SaveTrackingItemUnsafe(trackingItem); + + if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); + return resultItem; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public async Task SaveItem(Guid id, TrackingState trackingState) + { + //Monitor.Enter(_syncRoot); + + try + { + T resultItem = null!; + + if (TryGetValue(id, out var item)) + resultItem = await SaveItem(item, trackingState); + + if (resultItem == null) throw new NullReferenceException($"SaveItem; resultItem == null"); + return resultItem; + } + finally + { + //Monitor.Exit(_syncRoot); + } + } + + public Task SaveItem(T item, TrackingState trackingState) => SaveItemUnsafe(item, trackingState); + + protected Task SaveTrackingItemUnsafe(TrackingItem trackingItem) + => SaveItemUnsafe(trackingItem.CurrentValue, trackingItem.TrackingState); + + protected Task SaveTrackingItemUnsafeAsync(TrackingItem trackingItem) + => SaveItemUnsafeAsync(trackingItem.CurrentValue, trackingItem.TrackingState); + + protected async Task SaveItemUnsafe(T item, TrackingState trackingState) + { + var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); + if (messageTag == AcSignalRTags.None) throw new ArgumentException($"SaveItemUnsafe; messageTag == SignalRTags.None"); + + var result = await SignalRClient.PostDataAsync(messageTag, item); + if (result == null) throw new NullReferenceException($"SaveItemUnsafe; result == null"); + + await ProcessSavedResponseItem(result, trackingState); + + return result; + } + + protected Task SaveItemUnsafeAsync(T item, TrackingState trackingState) + { + var messageTag = SignalRCrudTags.GetMessageTagByTrackingState(trackingState); + if (messageTag == AcSignalRTags.None) return Task.CompletedTask; + + return SignalRClient.PostDataAsync(messageTag, item, response => + { + Monitor.Enter(_syncRoot); + + try + { + if (response.Status != SignalResponseStatus.Success || response.ResponseData == null) + { + if (TryRollbackItem(item.Id, out _)) return; + + throw new NullReferenceException($"SaveItemUnsafeAsync; result.Status != SignalResponseStatus.Success || result.ResponseData == null; Status: {SignalResponseStatus.Success}"); + } + + ProcessSavedResponseItem(response.ResponseData, trackingState).Forget(); + } + finally + { + Monitor.Exit(_syncRoot); + } + }); + } + + private async Task ProcessSavedResponseItem(T? resultItem, TrackingState trackingState) + { + if (resultItem == null) return; + + if (TryGetTrackingItem(resultItem.Id, out var trackingItem)) + TrackingItems.Remove(trackingItem); + + if (TryGetIndex(resultItem.Id, out var index)) + InnerList[index] = resultItem; + + var eventArgs = new ItemChangedEventArgs(resultItem, trackingState); + if (OnDataSourceItemChanged != null) await OnDataSourceItemChanged.Invoke(eventArgs); + } + + 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) + { + Monitor.Enter(_syncRoot); + try + { + if (TryGetTrackingItem(id, out var trackingItem)) + { + originalValue = trackingItem.OriginalValue; + + RollbackItemUnsafe(trackingItem); + return true; + } + + originalValue = null; + return false; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void Rollback() + { + Monitor.Enter(_syncRoot); + try + { + foreach (var trackingItem in TrackingItems.ToList()) + RollbackItemUnsafe(trackingItem); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public int Count + { + get + { + Monitor.Enter(_syncRoot); + try + { + return InnerList.Count; + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + } + + public void Clear() => Clear(true); + + public void Clear(bool clearChangeTracking) + { + Monitor.Enter(_syncRoot); + try + { + if (clearChangeTracking) TrackingItems.Clear(); + InnerList.Clear(); + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public int IndexOf(Guid id) + { + Monitor.Enter(_syncRoot); + try + { + return InnerList.FindIndex(x => x.Id == id); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + 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) + { + Monitor.Enter(_syncRoot); + try + { + item = InnerList.FirstOrDefault(x => x.Id == id); + return item != null; + } + finally + { + Monitor.Exit(_syncRoot); + } + } + + public void CopyTo(T[] array) => CopyTo(array, 0); + + public void CopyTo(T[] array, int arrayIndex) + { + Monitor.Enter(_syncRoot); + try + { + InnerList.CopyTo(array, arrayIndex); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + public int BinarySearch(int index, int count, T item, IComparer? comparer) + { + throw new NotImplementedException($"BinarySearch"); + if (index < 0) + throw new ArgumentOutOfRangeException(nameof(index)); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (Count - index < count) + throw new ArgumentException("Invalid length"); + + //Monitor.Enter(_syncRoot); + //try + //{ + // return InnerList.BinarySearch(index, count, item, comparer); + //} + //finally + //{ + // Monitor.Exit(_syncRoot); + //} + } + + public int BinarySearch(T item) => BinarySearch(0, Count, item, null); + public int BinarySearch(T item, IComparer? comparer) => BinarySearch(0, Count, item, comparer); + + public IEnumerator GetEnumerator() + { + Monitor.Enter(_syncRoot); + try + { + //return InnerList.ToList().GetEnumerator(); + return InnerList.GetEnumerator(); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + + public ReadOnlyCollection AsReadOnly() => new(this); + private static bool IsCompatibleObject(object? value) => (value is T) || (value == null && default(T) == null); + + + #region IList, ICollection + + bool IList.IsReadOnly => false; + + object? IList.this[int index] + { + get => this[index]; + set + { + if (default(T) != null && value == null) throw new NullReferenceException(nameof(value)); + + try + { + this[index] = (T)value!; + } + catch (InvalidCastException) + { + throw new InvalidCastException(nameof(value)); + } + } + } + + int IList.Add(object? item) + { + if (default(T) != null && item == null) throw new NullReferenceException(nameof(item)); + + try + { + Add((T)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((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)); + + try + { + Insert(index, (T)item!); + } + catch (InvalidCastException) + { + throw new InvalidCastException(nameof(item)); + } + } + + void IList.Remove(object? item) + { + if (IsCompatibleObject(item)) Remove((T)item!); + } + + void ICollection.Clear() => Clear(true); + + void ICollection.CopyTo(Array array, int arrayIndex) + { + if ((array != null) && (array.Rank != 1)) + { + throw new ArgumentException(); + } + + try + { + Monitor.Enter(_syncRoot); + try + { + //TODO: _list.ToArray() - ez nem az igazi... - J. + Array.Copy(InnerList.ToArray(), 0, array!, arrayIndex, InnerList.Count); + } + finally + { + Monitor.Exit(_syncRoot); + } + + } + 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 IList, ICollection + } + + public class ItemChangedEventArgs where T : IId + { + internal ItemChangedEventArgs(T item, TrackingState trackingState) + { + Item = item; + TrackingState = trackingState; + } + + public T Item { get; } + public TrackingState TrackingState { get; } + } +} diff --git a/AyCode.Blazor.Components/Services/TrackingItemHelpers.cs b/AyCode.Blazor.Components/Services/TrackingItemHelpers.cs new file mode 100644 index 0000000..bbd1ce1 --- /dev/null +++ b/AyCode.Blazor.Components/Services/TrackingItemHelpers.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using AyCode.Core.Extensions; + +namespace AyCode.Blazor.Components.Services; + +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 diff --git a/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj b/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj index 1b8afeb..4842669 100644 --- a/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj +++ b/AyCode.Blazor.Controllers/AyCode.Blazor.Controllers.csproj @@ -54,4 +54,10 @@ + + + + + + diff --git a/AyCode.Maui.Core/AyCode.Maui.Core.csproj b/AyCode.Maui.Core/AyCode.Maui.Core.csproj index acfca73..9038cba 100644 --- a/AyCode.Maui.Core/AyCode.Maui.Core.csproj +++ b/AyCode.Maui.Core/AyCode.Maui.Core.csproj @@ -17,6 +17,12 @@ 6.5 + + + + + + ..\..\AyCode.Core\AyCode.Services.Server\bin\Debug\net8.0\AyCode.Core.dll