diff --git a/TIAM.Services/SignalRTags.cs b/TIAM.Services/SignalRTags.cs index 398b13a0..684bfbd6 100644 --- a/TIAM.Services/SignalRTags.cs +++ b/TIAM.Services/SignalRTags.cs @@ -4,6 +4,8 @@ namespace TIAM.Services; public class SignalRTags : AcSignalRTags { + public const int None = 0; + public const int GetTransfer = 3; public const int GetTransfers = 4; public const int GetTransfersByContextId = 5; diff --git a/TIAMSharedUI/Pages/User/MyServiceProviders.razor b/TIAMSharedUI/Pages/User/MyServiceProviders.razor index fd04f340..d3ecd46e 100644 --- a/TIAMSharedUI/Pages/User/MyServiceProviders.razor +++ b/TIAMSharedUI/Pages/User/MyServiceProviders.razor @@ -68,7 +68,8 @@ PageSize="12" KeyFieldName="Id" - ValidationEnabled="false" + ValidationEnabled="false" + DetailRowDisplayMode="GridDetailRowDisplayMode.Always" CustomizeEditModel="Grid_CustomizeEditModel" EditMode="GridEditMode.EditRow" KeyboardNavigationEnabled="true"> @@ -81,8 +82,14 @@ - + + + + @{ + @(((Company)context.DataItem).Profile.Address.AddressText) + } + @@ -141,19 +148,13 @@ } - protected override async Task OnAfterRenderAsync(bool firstRender) - { - if (firstRender) - await _gridCompany.StartEditRowAsync(0); - } - void Grid_CustomizeEditModel(GridCustomizeEditModelEventArgs e) { if (e.IsNew) { var newEmployee = (Company)e.EditModel; newEmployee.Name = "John"; - newEmployee.OwnerId = Guid.NewGuid(); + newEmployee.AffiliateId = Guid.NewGuid(); } } diff --git a/TIAMSharedUI/Pages/User/SysAdmins/AddressDetailGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/AddressDetailGridComponent.razor index 2d99bc54..3a092984 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/AddressDetailGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/AddressDetailGridComponent.razor @@ -13,6 +13,7 @@ @using TIAMSharedUI.Pages.Components.EditComponents @using TIAMWebApp.Shared.Application.Services @using AyCode.Interfaces.Addresses +@using AyCode.Core @inject IServiceProviderDataService serviceProviderDataService @inject IUserDataService userDataService @inject ITransferDataService transferDataService @@ -25,9 +26,9 @@ DataSource="DataSource" Logger="_logger" SignalRClient="AdminSignalRClient" - OnEditModelSaving="DataItemSaving" - OnDataItemDeleting="DataItemDeleting" - OnDataItemChanged="DataItemChanged" + OnGridEditModelSaving="DataItemSaving" + OnGridItemDeleting="DataItemDeleting" + OnGridItemChanged="DataItemChanged" PageSize="5" AutoExpandAllGroupRows="true" KeyboardNavigationEnabled="KeyboardNavigationEnabled" @@ -37,15 +38,14 @@ ColumnResizeMode="GridColumnResizeMode.NextColumn" ShowFilterRow="false"> - - + + - - + + - - + @{ diff --git a/TIAMSharedUI/Pages/User/SysAdmins/AddressGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/AddressGridComponent.razor index 0168bab4..532c8c49 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/AddressGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/AddressGridComponent.razor @@ -11,6 +11,7 @@ @using TIAM.Entities.Addresses @using TIAMSharedUI.Shared.Components.Grids @using TIAMSharedUI.Pages.Components.EditComponents +@using AyCode.Core @inject IServiceProviderDataService serviceProviderDataService @inject IUserDataService userDataService @inject ITransferDataService transferDataService @@ -30,11 +31,11 @@ ColumnResizeMode="GridColumnResizeMode.NextColumn" ShowFilterRow="true"> - - + + - - + + diff --git a/TIAMSharedUI/Pages/User/SysAdmins/ManageServiceProviders.razor b/TIAMSharedUI/Pages/User/SysAdmins/ManageServiceProviders.razor index ab7f7ea7..18247ea6 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/ManageServiceProviders.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/ManageServiceProviders.razor @@ -12,6 +12,7 @@ @using AyCode.Core.Extensions; @using TIAM.Entities.Addresses @using TIAMSharedUI.Shared.Components.Grids +@using AyCode.Core @layout AdminLayout @inject IEnumerable LogWriters @inject IStringLocalizer localizer @@ -92,29 +93,29 @@ KeyFieldName="Id"> - - + + - + @{ var keyField = context.Value as Guid?; - var keyItem = (Company)context.DataItem; if (keyField.IsNullOrEmpty()) { - + } else { - @keyField + @keyField.Value.ToString("N") } } - + - + + @* *@ diff --git a/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor b/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor index 642b1107..c19e7045 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/ManageTransfers.razor @@ -18,6 +18,8 @@ @using AyCode.Core.Extensions @using AyCode.Core.Consts @using AyCode.Core +@using AyCode.Core.Helpers +@using TIAM.Entities.Emails @layout AdminLayout @inject IEnumerable LogWriters @inject IStringLocalizer localizer @@ -90,10 +92,10 @@ Logger="_logger" SignalRClient="AdminSignalRClient" OnDataSourceChanged="DataSourceChanged" - OnDataItemChanging="DataSourceItemChanging" - OnDataItemChanged="DataSourceItemChanged" - OnDataItemDeleting="DataItemDeleting" - OnEditModelSaving="DataItemSaving" + OnGridItemChanging="DataSourceItemChanging" + OnGridItemChanged="DataSourceItemChanged" + OnGridItemDeleting="DataItemDeleting" + OnGridEditModelSaving="DataItemSaving" CustomizeElement="Grid_CustomizeElement" CustomizeEditModel="Grid_CustomizeEditModel" @@ -105,22 +107,22 @@ KeyFieldName="Id"> - - + + @{ - var idKeyField = ((Transfer)context.DataItem).Id as Guid?; - var editUri = $"mytransfers/{idKeyField}"; + var idKeyField = ((Transfer)context.DataItem).Id; + var editUri = $"mytransfers/{idKeyField:N}"; - @context.Value + @context.Value } - + - + @@ -140,7 +142,7 @@ TransferStatusModel keyField = Statuses.FirstOrDefault(x => x.StatusValue == Convert.ToInt16(context.Value)); string transferStatusText = keyField.StatusName; -

@transferStatusText

+ @transferStatusText }
@@ -152,6 +154,31 @@ + + + + + + + + + + @System.Text.RegularExpressions.Regex.Replace((displayTextContext.Value as string)!, "<(.|\n)*?>", string.Empty) + + + + @{ + var value = ((EmailMessage)editTextContext.EditModel).Text; + + } + + + + + @@ -383,7 +410,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/ProfileGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/ProfileGridComponent.razor index 7fe56619..9250f1d7 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/ProfileGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/ProfileGridComponent.razor @@ -5,6 +5,7 @@ @using TIAMWebApp.Shared.Application.Utility @using AyCode.Services.Loggers @using TIAM.Core.Loggers +@using AyCode.Core @inject IServiceProviderDataService serviceProviderDataService @inject IUserDataService userDataService @inject IEnumerable LogWriters @@ -26,7 +27,7 @@ ShowFilterRow="true"> - + diff --git a/TIAMSharedUI/Pages/User/SysAdmins/ServiceProviderGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/ServiceProviderGridComponent.razor index f3c1a5e1..54fd580e 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/ServiceProviderGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/ServiceProviderGridComponent.razor @@ -8,6 +8,7 @@ @using TIAMWebApp.Shared.Application.Utility @using AyCode.Services.Loggers @using TIAM.Core.Loggers +@using AyCode.Core @inject IServiceProviderDataService ServiceProviderDataService @inject IEnumerable LogWriters @@ -27,8 +28,8 @@ ColumnResizeMode="GridColumnResizeMode.NextColumn" ShowFilterRow="true"> - - + + diff --git a/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor index 030b7790..9744e98a 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/TransferToDriverGridComponent.razor @@ -10,6 +10,7 @@ @using TIAMWebApp.Shared.Application.Services @using AyCode.Core.Enums @using AyCode.Core.Extensions +@using AyCode.Core @inject IUserDataService UserDataService @inject IEnumerable LogWriters @inject AdminSignalRClient AdminSignalRClient; @@ -20,9 +21,9 @@ DataSource="ParentData.TransferToDrivers" Logger="_logger" SignalRClient="AdminSignalRClient" - OnEditModelSaving="DataItemSaving" - OnDataItemDeleting="DataItemDeleting" - OnDataItemChanged="DataItemChanged" + OnGridEditModelSaving="DataItemSaving" + OnGridItemDeleting="DataItemDeleting" + OnGridItemChanged="DataItemChanged" PageSize="5" AutoExpandAllGroupRows="true" KeyboardNavigationEnabled="KeyboardNavigationEnabled" @@ -34,7 +35,7 @@ ShowFilterRow="false"> - + @@ -58,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/Pages/User/SysAdmins/UserProductMappingGridComponent.razor b/TIAMSharedUI/Pages/User/SysAdmins/UserProductMappingGridComponent.razor index 66afd837..82e3b586 100644 --- a/TIAMSharedUI/Pages/User/SysAdmins/UserProductMappingGridComponent.razor +++ b/TIAMSharedUI/Pages/User/SysAdmins/UserProductMappingGridComponent.razor @@ -8,6 +8,7 @@ @using TIAM.Core.Loggers @using AyCode.Core.Loggers @using AyCode.Services.Loggers +@using AyCode.Core @inject IServiceProviderDataService ServiceProviderDataService @inject IEnumerable LogWriters @@ -26,7 +27,7 @@ ShowFilterRow="true"> - + diff --git a/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs b/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs index d159ee18..f36fb4b7 100644 --- a/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs +++ b/TIAMSharedUI/Shared/Components/Grids/TiamGrid.cs @@ -8,41 +8,22 @@ using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DevExpress.Blazor; using Microsoft.AspNetCore.Components; +using TIAM.Services; using TIAMWebApp.Shared.Application.Services; using TIAMWebApp.Shared.Application.Utility; 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) - { } - - public bool IsCanceled { get; set; } - } - - public class GridDataItemChangedEventArgs where TDataItem : class, IId - { - internal GridDataItemChangedEventArgs(TiamGrid grid, TDataItem dataItem, DataChangeMode dataChangeMode) - { - Grid = grid; - DataItem = dataItem; - DataChangeMode = dataChangeMode; - } - - public TiamGrid Grid { get; } - public TDataItem DataItem { get; } - public DataChangeMode DataChangeMode { get; } - } - public class TiamGrid : DxGrid where TDataItem : class, IId { protected bool IsFirstInitializeParameters; - private IList _dataSource = null!; + private SignalRDataSource _dataSource = null!; + private IList _dataSourceParam = []; private string _gridLogName; public TiamGrid() : base() - { } + { + } [Parameter] public LoggerClient Logger { get; set; } [Parameter] public string GridName { get; set; } @@ -56,14 +37,19 @@ namespace TIAMSharedUI.Shared.Components.Grids [Parameter] public int RemoveMessageTag { get; set; } protected new EventCallback DataItemDeleting { get; set; } - [Parameter] public EventCallback OnDataItemDeleting{ get; set; } + [Parameter] public EventCallback OnGridItemDeleting { get; set; } protected new EventCallback EditModelSaving { get; set; } - [Parameter] public EventCallback OnEditModelSaving { get; set; } + [Parameter] public EventCallback OnGridEditModelSaving { get; set; } [Parameter] public EventCallback> OnDataSourceChanged { get; set; } - [Parameter] public EventCallback> OnDataItemChanging { get; set; } - [Parameter] public EventCallback> OnDataItemChanged { get; set; } + [Parameter] public EventCallback> OnGridItemChanging { get; set; } + + /// + /// After the server has responded! + /// + [Parameter] + public EventCallback> OnGridItemChanged { get; set; } [Parameter] [DefaultValue(null)] @@ -80,28 +66,49 @@ namespace TIAMSharedUI.Shared.Components.Grids return _dataSource!; } - set - { - if (value == null) throw new ArgumentNullException(nameof(value)); - - var equals = Equals(_dataSource, value); - - _dataSource = value; - Data = _dataSource; - - if (!equals) OnDataSourceChanged.InvokeAsync(_dataSource); - } + set => _dataSourceParam = value; } - protected override void OnInitialized() + protected override async Task OnInitializedAsync() { if (Logger == null) throw new NullReferenceException($"[{GetType().Name}] Logger == null"); if (SignalRClient == null) + { Logger.Error($"[{GetType().Name}] SignalRClient == null"); + throw new NullReferenceException($"[{GetType().Name}] SignalRClient == null"); + } - base.OnInitialized(); + var crudTags = new SignalRCrudTags(GetAllMessageTag, SignalRTags.None, AddMessageTag, UpdateMessageTag, RemoveMessageTag); + _dataSource = new SignalRDataSource(SignalRClient, crudTags, ContextId); + + Data = _dataSource; + + _dataSource.OnDataSourceLoaded += OnDataSourceLoaded; + _dataSource.OnDataSourceItemChanged += OnDataSourceItemChanged; + + await base.OnInitializedAsync(); + } + + private async Task OnDataSourceItemChanged(ItemChangedEventArgs args) + { + if (args.TrackingState is TrackingState.GetAll or TrackingState.None) return; + + Logger.Debug($"{_gridLogName} OnDataSourceItemChanged; trackingState: {args.TrackingState}"); + + var changedEventArgs = new GridDataItemChangedEventArgs(this, args.Item, args.TrackingState); + await OnGridItemChanged.InvokeAsync(changedEventArgs); + + await InvokeAsync(StateHasChanged); + } + + private Task OnDataSourceLoaded() + { + Logger.Debug($"{_gridLogName} OnDataSourceLoaded"); + + Reload(); + return OnDataSourceChanged.InvokeAsync(_dataSource); } protected override async Task OnAfterRenderAsync(bool firstRender) @@ -110,35 +117,35 @@ namespace TIAMSharedUI.Shared.Components.Grids if (firstRender) { - if (_dataSource == null || _dataSource.Count == 0) RefreshDataSourceAsync().Forget(); + if (_dataSourceParam.Count > 0) await _dataSource.LoadDataSource(_dataSourceParam); + else _dataSource.LoadDataSourceAsync(true).Forget(); } } - public Task AddDataItem(TDataItem dataItem) => AddDataItem(dataItem, AddMessageTag); - public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Add); - - public Task UpdateDataItem(TDataItem dataItem) => UpdateDataItem(dataItem, UpdateMessageTag); - public Task UpdateDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Update); - - public Task RemoveDataItem(TDataItem dataItem) => RemoveDataItem(dataItem, RemoveMessageTag); - public Task RemoveDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, DataChangeMode.Remove); - - public Task RemoveDataItem(Guid id) => RemoveDataItem(id, RemoveMessageTag); - public Task RemoveDataItem(Guid id, int messageTag) + public Task AddDataItem(TDataItem dataItem) { - var dataItem = _dataSource.FirstOrDefault(x => x.Id == id); + if (dataItem.Id.IsNullOrEmpty()) dataItem.Id = Guid.NewGuid(); + return _dataSource.Add(dataItem, true); + } - return dataItem == null ? Task.CompletedTask : RemoveDataItem(dataItem); + public Task AddDataItemAsync(TDataItem dataItem) + { + if (dataItem.Id.IsNullOrEmpty()) dataItem.Id = Guid.NewGuid(); + _dataSource.Add(dataItem); + + return SaveChangesToServerAsync(); } private async Task OnItemSaving(GridEditModelSavingEventArgs e) { var dataItem = (e.EditModel as TDataItem)!; - var logText = e.IsNew ? "add" : "update"; - Logger.Info($"{_gridLogName} OnItemSaving {logText}; Id: {dataItem.Id}"); + if (e.IsNew && dataItem.Id.IsNullOrEmpty()) dataItem.Id = Guid.NewGuid(); - await OnEditModelSaving.InvokeAsync(e); + var logText = e.IsNew ? "add" : "update"; + Logger.Debug($"{_gridLogName} OnItemSaving {logText}; Id: {dataItem.Id}"); + + await OnGridEditModelSaving.InvokeAsync(e); if (e.Cancel) { @@ -146,79 +153,70 @@ namespace TIAMSharedUI.Shared.Components.Grids return; } - if (e.IsNew) await AddDataItem(dataItem); - else await UpdateDataItem(dataItem); + if (e.IsNew) await AddDataItemAsync(dataItem); + else await UpdateDataItemAsync(dataItem); + } + + private Task SaveChangesToServerAsync() + { + try + { + return _dataSource.SaveChangesAsync(); + } + catch (Exception ex) + { + Logger.Error($"{_gridLogName} SaveChangesToServerAsync->SaveChangesAsync error!", ex); + } + + return Task.CompletedTask; + } + + private async Task SaveChangesToServer() + { + var result = false; + + try + { + var unsavedItems = await _dataSource.SaveChanges(); + + if ((result = unsavedItems.Count == 0) == false) + Logger.Error($"{_gridLogName} SaveChangesToServer->SaveChanges error! unsavedCount: {unsavedItems.Count}"); + } + catch (Exception ex) + { + Logger.Error($"{_gridLogName} OnItemSaving", ex); + } + + return result; } private async Task OnItemDeleting(GridDataItemDeletingEventArgs e) { - Logger.Info($"{_gridLogName} OnItemDeleting"); + Logger.Debug($"{_gridLogName} OnItemDeleting"); - await OnDataItemDeleting.InvokeAsync(e); + await OnGridItemDeleting.InvokeAsync(e); if (e.Cancel) { Logger.Debug($"{_gridLogName} OnItemDeleting canceled"); return; } - + var dataItem = (e.DataItem as TDataItem)!; await RemoveDataItem(dataItem); } - public virtual Task RefreshDataSourceAsync() + private void OnCustomizeElement(GridCustomizeElementEventArgs e) { - if (GetAllMessageTag == 0) return Task.CompletedTask; - - Logger.Info($"{_gridLogName} RefreshDataSourceAsync called"); - - return SignalRClient.GetAllAsync>(GetAllMessageTag, ContextId, response => + if (e.ElementType == GridElementType.DetailCell) { - if (response.Status == SignalResponseStatus.Error) - return; - - BeginUpdate(); - DataSource = response.ResponseData ?? []; - EndUpdate(); - - InvokeAsync(StateHasChanged).Forget(); - }); - } - - protected virtual async Task PostDataToServerAsync(TDataItem dataItem, int messageTag, DataChangeMode dataChangeMode) - { - var eventArgs = new GridDataItemChangingEventArgs(this, dataItem, dataChangeMode); - await OnDataItemChanging.InvokeAsync(eventArgs); - - if (eventArgs.IsCanceled) - { - Logger.Debug($"{_gridLogName} OnDataItemChanging canceled"); - return; + e.Style = "padding: 0.5rem; opacity: 0.75"; } - - if (messageTag == 0) return; - - Logger.Info($"{_gridLogName} PostDataToServerAsync called; transferId " + dataItem.Id); - - 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. - - SignalRClient.PostDataAsync(messageTag, dataItem, async repsonse => + else if (false && e.ElementType == GridElementType.DataCell && e.Column.Name == nameof(IId.Id)) { - if (repsonse.Status != SignalResponseStatus.Success || repsonse.ResponseData == null) - { - RefreshDataSourceAsync().Forget(); - return; - } - - _dataSource.UpdateCollection(repsonse.ResponseData, dataChangeMode == DataChangeMode.Remove); - - await OnDataItemChanged.InvokeAsync(eventArgs); - InvokeAsync(StateHasChanged).Forget(); - }).Forget(); - - //transfer = await devAdminSignalClient.PostDataAsync(SignalRTags.UpdateTransferAsync, transfer); + e.Column.Visible = AcDomain.IsDeveloperVersion; + e.Column.ShowInColumnChooser = AcDomain.IsDeveloperVersion; + } } protected override Task SetParametersAsyncCore(ParameterView parameters) @@ -227,7 +225,9 @@ namespace TIAMSharedUI.Shared.Components.Grids { base.DataItemDeleting = EventCallback.Factory.Create(this, OnItemDeleting); base.EditModelSaving = EventCallback.Factory.Create(this, OnItemSaving); - + + CustomizeElement += OnCustomizeElement; + //ShowFilterRow = true; //PageSize = 4; //ShowGroupPanel = true; @@ -235,7 +235,6 @@ namespace TIAMSharedUI.Shared.Components.Grids TextWrapEnabled = false; - //var columns = GetColumns(); //var dataColumns = GetDataColumns(); //var idColumn = dataColumns.FirstOrDefault(x => x.FieldName == nameof(IId.Id)); @@ -251,6 +250,7 @@ namespace TIAMSharedUI.Shared.Components.Grids return base.SetParametersAsyncCore(parameters); } + protected override void OnParametersSet() { base.OnParametersSet(); @@ -260,5 +260,54 @@ namespace TIAMSharedUI.Shared.Components.Grids _gridLogName = $"[{GridName}]"; } + + //public Task AddDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Add); + + public Task UpdateDataItem(TDataItem dataItem) => _dataSource.Update(dataItem, true); + + public Task UpdateDataItemAsync(TDataItem dataItem) + { + _dataSource.Update(dataItem, false); + return SaveChangesToServerAsync(); + } + //public Task UpdateDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Update); + + public Task AddOrUpdateDataItem(TDataItem dataItem) => _dataSource.AddOrUpdate(dataItem, true); + + public Task RemoveDataItem(TDataItem dataItem) => _dataSource.Remove(dataItem, true); + //public Task RemoveDataItem(TDataItem dataItem, int messageTag) => PostDataToServerAsync(dataItem, messageTag, TrackingState.Remove); + + public Task RemoveDataItem(Guid id) => RemoveDataItem(id, RemoveMessageTag); + + public Task RemoveDataItem(Guid id, int messageTag) + { + var dataItem = _dataSource.FirstOrDefault(x => x.Id == id); + if (dataItem == null) return Task.CompletedTask; + + return _dataSource.Remove(dataItem, true); + } + } + + public class GridDataItemChangingEventArgs : GridDataItemChangedEventArgs where TDataItem : class, IId + { + internal GridDataItemChangingEventArgs(TiamGrid grid, TDataItem dataItem, TrackingState trackingState) : base(grid, dataItem, trackingState) + { + } + + public bool IsCanceled { get; set; } + } + + public class GridDataItemChangedEventArgs where TDataItem : class, IId + { + internal GridDataItemChangedEventArgs(TiamGrid grid, TDataItem dataItem, TrackingState trackingState) + { + Grid = grid; + DataItem = dataItem; + TrackingState = trackingState; + } + + public TiamGrid Grid { get; } + public TDataItem DataItem { get; } + public TrackingState TrackingState { get; } } } 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/Server/Services/DevAdminSignalRhub.cs b/TIAMWebApp/Server/Services/DevAdminSignalRhub.cs index 07d738f7..be9b565f 100644 --- a/TIAMWebApp/Server/Services/DevAdminSignalRhub.cs +++ b/TIAMWebApp/Server/Services/DevAdminSignalRhub.cs @@ -14,6 +14,10 @@ using System.Runtime.CompilerServices; using MessagePack; using TIAM.Entities.Addresses; using TIAM.Entities.Profiles; +using Microsoft.AspNetCore.Hosting; +using System.Collections.Generic; +using TIAM.Entities.Emails; +using TIAM.Services.Server; namespace TIAMWebApp.Server.Services; @@ -57,7 +61,11 @@ public class DynamicMethodCallModel where TAttribute : TagAttribute public object InstanceObject { get; init; } public ConcurrentDictionary> MethodsByMessageTag { get; init; } = new(); - public DynamicMethodCallModel(Type instanceObjectType) : this(Activator.CreateInstance(instanceObjectType)!) + + public DynamicMethodCallModel(Type instanceObjectType) : this(instanceObjectType, null!) + { } + + public DynamicMethodCallModel(Type instanceObjectType, params object[] constructorParams) : this(Activator.CreateInstance(instanceObjectType, constructorParams)!) { } public DynamicMethodCallModel(object instanceObject) @@ -75,7 +83,7 @@ public class DynamicMethodCallModel where TAttribute : TagAttribute public class DevAdminSignalRHub : Hub, IAcSignalRHubServer { - private readonly List> _dynamicMethodCallModels = new(); + private readonly List> _dynamicMethodCallModels = []; private readonly TIAM.Core.Loggers.Logger _logger; private readonly AdminDal _adminDal; @@ -93,6 +101,7 @@ public class DevAdminSignalRHub : Hub, IAcSignalRHubServe _dynamicMethodCallModels.Add(new DynamicMethodCallModel(serviceProviderApiController)); _dynamicMethodCallModels.Add(new DynamicMethodCallModel(transferDataApiController)); _dynamicMethodCallModels.Add(new DynamicMethodCallModel(messageApiController)); + //_dynamicMethodCallModels.Add(new DynamicMethodCallModel(typeof(AdminDal))); } diff --git a/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs b/TIAMWebApp/Shared/Services/AcSignalRClientBase.cs index cefac36e..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,18 +87,21 @@ 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); - public virtual Task GetAllAsync(int messageTag) where TResponse : class - => SendMessageToServerAsync(messageTag); + 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, Guid? contextId, Action> responseCallback) => SendMessageToServerAsync(messageTag, (contextId.IsNullOrEmpty() ? null : new SignalPostJsonDataMessage(new IdMessage(contextId.Value))), responseCallback); + public virtual Task GetAllAsync(int messageTag, Guid? contextId) where TResponseData : class + => SendMessageToServerAsync(messageTag, contextId.IsNullOrEmpty() ? null : new SignalPostJsonDataMessage(new IdMessage(contextId.Value)), 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 @@ -114,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 new file mode 100644 index 00000000..fc9a14f3 --- /dev/null +++ b/TIAMWebApp/Shared/Utility/SignalRDataSource.cs @@ -0,0 +1,1034 @@ +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.Helpers; +using AyCode.Core.Interfaces; +using AyCode.Services.SignalRs; +using TIAM.Services; +using TIAMWebApp.Shared.Application.Services; + +namespace TIAMWebApp.Shared.Application.Utility +{ + 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 SignalRDataSource : 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? ContextId; + public AcSignalRClientBase SignalRClient; + protected readonly SignalRCrudTags SignalRCrudTags; + + public Func, Task>? OnDataSourceItemChanged; + public Func? OnDataSourceLoaded; + + public SignalRDataSource(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, Guid? contextId = null) + { + ContextId = contextId; + + 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 == SignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + var resultList = (await SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, ContextId)) ?? throw new NullReferenceException(); + + await LoadDataSource(resultList); + } + + public Task LoadDataSourceAsync(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == SignalRTags.None) throw new ArgumentException($"SignalRCrudTags.GetAllMessageTag == SignalRTags.None"); + + return SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, ContextId, 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(); + }); + } + + 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 == SignalRTags.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 == SignalRTags.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 == SignalRTags.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/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs b/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs new file mode 100644 index 00000000..d435cb51 --- /dev/null +++ b/TIAMWebApp/Shared/Utility/SignalRDataSourceAsync.cs @@ -0,0 +1,104 @@ +using System.Diagnostics; +using AyCode.Core.Enums; +using AyCode.Core.Helpers; +using AyCode.Core.Interfaces; +using AyCode.Services.SignalRs; +using TIAM.Services; +using TIAMWebApp.Shared.Application.Services; + +namespace TIAMWebApp.Shared.Application.Utility; + +[Serializable] +[DebuggerDisplay("Count = {Count}")] +public class SignalRDataSourceAsync : SignalRDataSource where T : class, IId +{ + public Action>? OnItemChanged; + public Action>? OnDataSourceLoaded; + + public SignalRDataSourceAsync(AcSignalRClientBase signalRClient, SignalRCrudTags signalRCrudTags, Guid? contextId = null, Action>? onDataSourceLoaded = null, bool autoLoadDataSource = false) + : base(signalRClient, signalRCrudTags, contextId) + { + OnDataSourceLoaded = onDataSourceLoaded; + + if (autoLoadDataSource) LoadDataSourceAsync(); + } + + public void LoadDataSourceAsync(bool clearChangeTracking = true) + { + if (SignalRCrudTags.GetAllMessageTag == SignalRTags.None) throw new ArgumentException($"_signalRCrudTags.GetAllMessageTag == SignalRTags.None;"); + + //Monitor.Exit(SyncRoot); //Exception test - J. + + //Monitor.Enter(SyncRoot); + //try + //{ + // SignalRClient.GetAllAsync>(SignalRCrudTags.GetAllMessageTag, ContextId, response => + // { + // try + // { + // if (response.Status == SignalResponseStatus.Error) throw new Exception($"LoadDataSourceAsync; response.Status == SignalResponseStatus.Error"); + // if (response.ResponseData == null) throw new NullReferenceException($"response.ResponseData == null"); + + // Clear(clearChangeTracking); + // InnerList.AddRange(response.ResponseData); + // } + // finally + // { + // Monitor.Exit(SyncRoot); + // } + + // OnDataSourceLoaded?.Invoke(this); + // }).Forget(); + //} + //catch (Exception) + //{ + // Monitor.Exit(SyncRoot); + // throw; + //} + } + + + //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, TrackingState.Update); + + //public Task RemoveAsync(T item, int messageTag) => PostDataToServerAsync(item, messageTag, TrackingState.Remove); + + //public Task RemoveAsync(Guid id, int messageTag) + //{ + // var item = _list.FirstOrDefault(x => x.Id == id); + + // return item == null ? Task.CompletedTask : RemoveAsync(item, messageTag); + //} + + //protected virtual Task PostDataToServerAsync(T item, int messageTag, TrackingState trackingState) + //{ + // if (messageTag == 0) return Task.CompletedTask; + + // logger.Info($"{_listLogName} PostDataToServerAsync called; transferId " + item.Id); + + // if (item.Id.IsNullOrEmpty()) item.Id = Guid.NewGuid(); + + // _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 => + // { + // if (repsonse.Status != SignalResponseStatus.Success || repsonse.ResponseData == null) + // { + // RefreshDataSourceAsync().Forget(); + // return; + // } + + // _list.UpdateCollection(repsonse.ResponseData, trackingState == TrackingState.Remove); + + // var eventArgs = new ItemChangedEventArgs(repsonse.ResponseData, trackingState); + // OnItemChanged.Invoke(eventArgs); + // }); + + // //transfer = await devAdminSignalClient.PostDataAsync(SignalRTags.UpdateTransferAsync, transfer); + + // return Task.CompletedTask; + //} +} \ 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