Add MgLazyLoadContent, grid layout refactor, tests

- Introduced MgLazyLoadContent component with JS observer for lazy rendering of heavy content.
- Refactored MgGridBase for user-specific, customizable layout persistence using JS interop.
- Improved MgGridInfoPanel caching and state batching for performance.
- Updated PDF viewer to use lazy loading for better UX.
- Added AyCode.Blazor.Components.Tests project with bUnit/MSTest grid layout tests.
- Updated solution/project files and removed obsolete code.
- Minor UI and JS module loading improvements.
This commit is contained in:
Loretta 2025-12-21 16:29:37 +01:00
parent 271868b4d5
commit 324f171377
5 changed files with 53 additions and 90 deletions

View File

@ -1,40 +1,28 @@
using AyCode.Blazor.Components.Components.Grids; using AyCode.Blazor.Components.Components.Grids;
using AyCode.Core.Extensions;
using AyCode.Core.Interfaces; using AyCode.Core.Interfaces;
using AyCode.Interfaces.Entities;
using AyCode.Utils.Extensions;
using DevExpress.Blazor; using DevExpress.Blazor;
using FruitBank.Common.Models; using FruitBank.Common.Models;
using FruitBankHybrid.Shared.Services.Loggers; using FruitBankHybrid.Shared.Services.Loggers;
using FruitBankHybrid.Shared.Services.SignalRs; using FruitBankHybrid.Shared.Services.SignalRs;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
namespace FruitBankHybrid.Shared.Components.Grids; namespace FruitBankHybrid.Shared.Components.Grids;
//var a = new GridDevExtremeDataSource(DataSource.AsQueryable().Where(x=>x.IsMeasurable));
public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient> where TDataItem : class, IId<int> public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient> where TDataItem : class, IId<int>
{ {
[Inject] public required LoggedInModel LoggedInModel { get; set; } [Inject] public required LoggedInModel LoggedInModel { get; set; }
[Inject] public required IJSRuntime JSRuntime { get; set; }
//[Parameter] public bool IsMasterGrid { get; set; } = false;
[Parameter] public string AutoSaveLayoutName { get; set; }
private bool _isFirstInitializeParameterCore; private bool _isFirstInitializeParameterCore;
private bool _isFirstInitializeParameters; private bool _isFirstInitializeParameters;
public bool PreRendered { get; set; } public bool PreRendered { get; set; }
//public virtual Task ReloadDataFromDb(bool forceReload = false) /// <summary>
//{ /// Override to provide the logged-in user's ID for layout storage
// throw new NotImplementedException(); /// </summary>
//} protected override int GetLayoutUserId() => LoggedInModel.CustomerDto?.Id ?? 0;
protected void OnCustomizeElement(GridCustomizeElementEventArgs e) protected void OnCustomizeElement(GridCustomizeElementEventArgs e)
{ {
//if (!IsMasterGrid) e.CssClass = "hideDetailButton";
if (IsMasterGrid && e.ElementType == GridElementType.DataRow && e.VisibleIndex % 2 == 1 && !e.Grid.IsRowSelected(e.VisibleIndex) && !e.Grid.IsRowFocused(e.VisibleIndex)) if (IsMasterGrid && e.ElementType == GridElementType.DataRow && e.VisibleIndex % 2 == 1 && !e.Grid.IsRowSelected(e.VisibleIndex) && !e.Grid.IsRowFocused(e.VisibleIndex))
{ {
e.CssClass = " alt-item"; e.CssClass = " alt-item";
@ -48,28 +36,21 @@ public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservab
if (e.ElementType == GridElementType.HeaderCell) if (e.ElementType == GridElementType.HeaderCell)
{ {
e.Style = "background-color: #E6E6E6;"; e.Style = "background-color: #E6E6E6;";
//e.CssClass = "header-bold";
} }
} }
protected override async Task SetParametersAsyncCore(ParameterView parameters) protected override async Task SetParametersAsyncCore(ParameterView parameters)
{ {
await base.SetParametersAsyncCore(parameters); await base.SetParametersAsyncCore(parameters);
if (!_isFirstInitializeParameterCore) if (!_isFirstInitializeParameterCore)
{ {
//if (typeof(TDataItem) is IId<Guid> || typeof(TDataItem) is IId<int>)
KeyFieldName = "Id"; KeyFieldName = "Id";
//base.DataItemDeleting = EventCallback.Factory.Create<GridDataItemDeletingEventArgs>(this, OnItemDeleting);
//base.EditModelSaving = EventCallback.Factory.Create<GridEditModelSavingEventArgs>(this, OnItemSaving);
CustomizeElement += OnCustomizeElement; CustomizeElement += OnCustomizeElement;
_isFirstInitializeParameterCore = true; _isFirstInitializeParameterCore = true;
} }
} }
protected override void OnParametersSet() protected override void OnParametersSet()
{ {
base.OnParametersSet(); base.OnParametersSet();
@ -102,14 +83,8 @@ public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservab
EditMode = GridEditMode.EditRow; EditMode = GridEditMode.EditRow;
FocusedRowEnabled = true; FocusedRowEnabled = true;
ColumnResizeMode = GridColumnResizeMode.NextColumn; ColumnResizeMode = GridColumnResizeMode.NextColumn;
//VirtualScrollingEnabled = IsMasterGrid;
PageSizeSelectorVisible = true; PageSizeSelectorVisible = true;
if (AutoSaveLayoutName.IsNullOrWhiteSpace()) AutoSaveLayoutName = $"Grid{typeof(TDataItem).Name}";
LayoutAutoLoading = Grid_LayoutAutoLoading;
LayoutAutoSaving = Grid_LayoutAutoSaving;
_isFirstInitializeParameters = true; _isFirstInitializeParameters = true;
} }
} }
@ -117,55 +92,5 @@ public class FruitBankGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservab
protected override void OnAfterRender(bool firstRender) protected override void OnAfterRender(bool firstRender)
{ {
base.OnAfterRender(firstRender); base.OnAfterRender(firstRender);
if (firstRender)
{
//PreRendered = true;
//StateHasChanged();
}
}
async Task Grid_LayoutAutoLoading(GridPersistentLayoutEventArgs e)
{
var masterDetailName = IsMasterGrid ? "Master" : ParentDataItem!.GetType().Name;
e.Layout = await LoadLayoutFromLocalStorageAsync($"{AutoSaveLayoutName}_{masterDetailName}_AutoSave_{LoggedInModel.CustomerDto?.Id ?? 0}");
}
private async Task Grid_LayoutAutoSaving(GridPersistentLayoutEventArgs e)
{
var masterDetailName = IsMasterGrid ? "Master" : ParentDataItem!.GetType().Name;
await SaveLayoutToLocalStorageAsync(e.Layout, $"{AutoSaveLayoutName}_{masterDetailName}_AutoSave_{LoggedInModel.CustomerDto?.Id ?? 0}");
}
async Task<GridPersistentLayout?> LoadLayoutFromLocalStorageAsync(string localStorageKey)
{
try
{
var json = await JSRuntime.InvokeAsync<string>("localStorage.getItem", localStorageKey);
if (!json.IsNullOrWhiteSpace()) return json.JsonTo<GridPersistentLayout>();
}
catch
{
// Mute exceptions for the server prerender stage
}
return null;
}
async Task SaveLayoutToLocalStorageAsync(GridPersistentLayout layout, string localStorageKey)
{
try
{
var json = layout.ToJson();
await JSRuntime.InvokeVoidAsync("localStorage.setItem", localStorageKey, json);
}
catch
{
// Mute exceptions for the server prerender stage
} }
} }
}
//public abstract class FruitBankObservableGridBase<TDataItem> : MgGridBase<SignalRDataSourceObservable<TDataItem>, TDataItem, int, LoggerClient> where TDataItem : class, IId<int>
//{ }

View File

@ -1,4 +1,5 @@
@using AyCode.Blazor.Components.Components.Grids @using AyCode.Blazor.Components.Components.Grids
@using AyCode.Blazor.Components.Components
@using DevExpress.Blazor @using DevExpress.Blazor
@using FruitBank.Common.Entities @using FruitBank.Common.Entities
@using System.IO @using System.IO
@ -37,7 +38,7 @@
<tr> <tr>
<th>Név a dokumentumon</th> <th>Név a dokumentumon</th>
<th>Termék neve</th> <th>Termék neve</th>
<th class="text-end">Rakl.</th> <th>Rakl.</th>
<th class="text-end">Menny.</th> <th class="text-end">Menny.</th>
<th class="text-end">Net.súly</th> <th class="text-end">Net.súly</th>
<th class="text-end">Br.súly</th> <th class="text-end">Br.súly</th>
@ -68,8 +69,14 @@
</tfoot> </tfoot>
</table> </table>
<div id="pdfContainer" style="width: 100%; height: 800px; overflow-y: auto; margin-top: 30px;"> <MgLazyLoadContent @ref="_lazyContentRef"
MinHeight="800px"
RootMargin="50px"
OnContentVisible="OnPdfContainerVisibleAsync"
ContainerStyle="margin-top: 30px;">
<div id="pdfContainer" style="width: 100%; height: 800px; overflow-y: auto;">
</div> </div>
</MgLazyLoadContent>
} }
</AfterColumnsTemplate> </AfterColumnsTemplate>
@ -90,11 +97,35 @@
"3_BP-30M35_20251113_163816.pdf" "3_BP-30M35_20251113_163816.pdf"
]; ];
private MgLazyLoadContent? _lazyContentRef;
private string? _currentPdfToRender;
private string _randomPdf;
protected override void OnInitialized()
{
base.OnInitialized();
}
private async Task OnDataItemChangedAsync(object? dataItem) private async Task OnDataItemChangedAsync(object? dataItem)
{ {
// Véletlenszerű PDF kiválasztása minden sor váltáskor // Store the PDF to render
var randomPdf = _pdfFiles[Random.Shared.Next(_pdfFiles.Length)]; _randomPdf = _pdfFiles[Random.Shared.Next(_pdfFiles.Length)];
var pdfUrls = new[] { $"_content/FruitBankHybrid.Shared/uploads/{randomPdf}" }; _currentPdfToRender = $"_content/FruitBankHybrid.Shared/uploads/{_randomPdf}";
// If MgLazyLoadContent is already visible, render the PDF immediately
if (_lazyContentRef is { IsVisible: true })
{
await _lazyContentRef.TriggerContentVisibleAsync();
}
}
private async Task OnPdfContainerVisibleAsync()
{
// Render PDF when container becomes visible OR when data changes
if (!string.IsNullOrEmpty(_currentPdfToRender))
{
var pdfUrls = new[] { _currentPdfToRender };
await JS.InvokeVoidAsync("pdfViewer.renderPdfs", "pdfContainer", pdfUrls); await JS.InvokeVoidAsync("pdfViewer.renderPdfs", "pdfContainer", pdfUrls);
} }
} }
}

View File

@ -26,6 +26,7 @@
<body class="dxbl-theme-fluent"> <body class="dxbl-theme-fluent">
<Routes @rendermode="InteractiveWebAssembly" /> <Routes @rendermode="InteractiveWebAssembly" />
<script src="_content/AyCode.Blazor.Components/js/mgGridInfoPanel.js"></script> <script src="_content/AyCode.Blazor.Components/js/mgGridInfoPanel.js"></script>
<script src="_content/AyCode.Blazor.Components/js/lazyContentObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script> <script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';

View File

@ -1,7 +1,7 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 18 # Visual Studio Version 18
VisualStudioVersion = 18.0.11222.15 d18.0 VisualStudioVersion = 18.0.11222.15
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruitBankHybrid", "FruitBankHybrid\FruitBankHybrid.csproj", "{85ADEDE3-C271-47DF-B273-2EDB32792CEF}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FruitBankHybrid", "FruitBankHybrid\FruitBankHybrid.csproj", "{85ADEDE3-C271-47DF-B273-2EDB32792CEF}"
EndProject EndProject
@ -31,9 +31,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Maui.Core", "..\..\.
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt = ..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components\Components\Grids\MgGridSignalRDataSource.txt
SqlSchemaCompare_Dev_to_Prod.scmp = SqlSchemaCompare_Dev_to_Prod.scmp SqlSchemaCompare_Dev_to_Prod.scmp = SqlSchemaCompare_Dev_to_Prod.scmp
EndProjectSection EndProjectSection
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AyCode.Blazor.Components.Tests", "..\..\..\Aycode\Source\AyCode.Blazor\AyCode.Blazor.Components.Tests\AyCode.Blazor.Components.Tests.csproj", "{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -73,7 +76,6 @@ Global
{4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4E4E4917-1CA3-A7D7-40A8-A24A08673EC1}.Release|Any CPU.Build.0 = Release|Any CPU
{5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Debug|Any CPU.Build.0 = Debug|Any CPU {5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Release|Any CPU.ActiveCfg = Release|Any CPU {5CE8B5A7-5390-61E4-33B3-FA5F0B75A168}.Release|Any CPU.ActiveCfg = Release|Any CPU
@ -96,6 +98,9 @@ Global
{F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.ActiveCfg = Release|Any CPU {F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.Build.0 = Release|Any CPU {F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.Build.0 = Release|Any CPU
{F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.Deploy.0 = Release|Any CPU {F7C67754-A59C-C355-2A10-C614F0585627}.Release|Any CPU.Deploy.0 = Release|Any CPU
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EFD44C6-DC9E-FEB8-F229-3E07C2E224FA}.Release|Any CPU.ActiveCfg = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@ -26,6 +26,7 @@
<script src="_framework/blazor.webview.js" autostart="false"></script> <script src="_framework/blazor.webview.js" autostart="false"></script>
<script src="_content/AyCode.Blazor.Components/js/mgGridInfoPanel.js"></script> <script src="_content/AyCode.Blazor.Components/js/mgGridInfoPanel.js"></script>
<script src="_content/AyCode.Blazor.Components/js/lazyContentObserver.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
<script> <script>
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';