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