From 6f4013c475eea6ecef0fc55e3c41d7f360b4212a Mon Sep 17 00:00:00 2001 From: Roland Banks Date: Wed, 11 Nov 2026 15:08:56 +0700 Subject: [PATCH 1/2] Bump .NET 10 servicing packages from 10.0.14 to 10.0.15 GHSA-x33c-w48q-qrgc (CVE-2026-114149, ASP.NET Core denial of service, high) covers Microsoft.AspNetCore.WebUtilities up to 10.0.14. With NuGetAuditMode All and warnings as errors, restore fails on main with NU1903 until it is bumped. 10.0.15 is the first patched version; the other 10.0.14 pins move with it. Co-Authored-By: Claude Opus 5.5 --- Directory.Packages.props | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 18f982ee0..609579e8b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,17 +10,17 @@ - - - - - - + + + + + + - + From d0dc9c7cef814e21a021aba89102e0f190960e7f Mon Sep 17 00:00:00 2001 From: Roland Banks Date: Wed, 11 Nov 2026 15:08:57 +0700 Subject: [PATCH 2/2] PDFileExplorer: NavigateToFolderAsync; context menus placed fixed and guarded; a hidden preview fetches nothing - PDFileExplorer.NavigateToFolderAsync(folderPath) opens a folder itself, loading only the folders above it, so the table loads once and FolderChanged is raised once. It shares the loading with NavigateToItemAsync. Demo row and docs. - PDContextMenu asked popper for fixed positioning with popper 1's "positionFixed", which popper 2 ignores, so menus were absolute: placed away from the pointer under CSS zoom or a scaled parent, and clipped by panels that scroll. It now uses strategy "fixed", with z-index var(--pd-contextmenu-z-index, 1000) so it sits above sticky headers. - PDContextMenu gains an opt-in ExceptionHandler: a failure in showing the menu or an item's click, such as its script failing, is passed to it rather than ending the circuit. Without one it is thrown as before. PDFileExplorer passes its handler to both of its menus. - PDFilePreview.IsVisible: while false nothing is fetched, and the item chosen meanwhile is fetched once it shows. PDFileExplorer passes the preview panel's visibility, so a hidden preview no longer fetches on every selection. - PDChatTests: the interrupt test fired its input outside InvokeAsync, racing a re-render (UnknownEventHandlerId). Co-Authored-By: Claude Opus 5.5 --- .../Pages/PDFileExplorerDocumentation.razor | 1 + .../Pages/PDFileExplorerPage.razor | 7 ++ .../Pages/PDFileExplorerPage.razor.cs | 11 +++ .../Components/PDChatTests.Interrupt.cs | 2 +- .../Components/PDContextMenuTests.Failures.cs | 44 ++++++++++++ .../PDFileExplorerTests.Failures.cs | 13 ++++ .../PDFileExplorerTests.Navigation.cs | 68 +++++++++++++++++++ .../Components/PDFileExplorerTests.Preview.cs | 29 ++++++++ .../Components/PDFilePreviewTests.cs | 17 +++++ PanoramicData.Blazor/PDContextMenu.razor.cs | 22 +++++- PanoramicData.Blazor/PDContextMenu.razor.css | 2 + PanoramicData.Blazor/PDContextMenu.razor.js | 4 +- .../PDFileExplorer.Operations.cs | 3 + PanoramicData.Blazor/PDFileExplorer.razor | 3 + PanoramicData.Blazor/PDFileExplorer.razor.cs | 64 ++++++++++++++--- PanoramicData.Blazor/PDFilePreview.razor.cs | 9 ++- 16 files changed, 283 insertions(+), 16 deletions(-) create mode 100644 PanoramicData.Blazor.Test/Components/PDContextMenuTests.Failures.cs diff --git a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerDocumentation.razor b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerDocumentation.razor index bb674132c..7d0f9bbe6 100644 --- a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerDocumentation.razor +++ b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerDocumentation.razor @@ -64,6 +64,7 @@ NavigateToAsync(path)Opens a folder by opening each folder above it in turn NavigateToItemAsync(itemPath)Opens the folder holding a file or folder and selects it, loading the folder's contents once. Returns false, leaving the deepest folder found open, when it is not there + NavigateToFolderAsync(folderPath)Opens the folder itself, loading its contents once, without first showing the folder holding it. Returns false, leaving the deepest folder found open, when it is not there diff --git a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor index 9855c9592..5328d6023 100644 --- a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor +++ b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor @@ -88,6 +88,13 @@ @_showItemResult +
+ Open a folder: + + + @_openFolderResult +
+

Navigate to /Library/Templates/ or /Sharepoint/Public/ to see read-only files. Try different combinations to compare: position (before/after), style (text/icon), and badge overlay. diff --git a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs index e88ab9076..4d2fa4906 100644 --- a/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs +++ b/PanoramicData.Blazor.Demo/Pages/PDFileExplorerPage.razor.cs @@ -95,6 +95,17 @@ private async Task ShowItemAsync() _showItemResult = isFound ? "Selected" : "Not found: the deepest folder found is open"; } + private string FolderPathToOpen { get; set; } = "/Library/Templates"; + + private string? _openFolderResult; + + // Opens the folder itself once, without first showing the folder holding it + private async Task OpenFolderAsync() + { + var isFound = await FileExplorer!.NavigateToFolderAsync(FolderPathToOpen).ConfigureAwait(true); + _openFolderResult = isFound ? "Opened" : "Not found: the deepest folder found is open"; + } + private async Task OnReady() { if (!string.IsNullOrWhiteSpace(_deepLinkPath)) diff --git a/PanoramicData.Blazor.Test/Components/PDChatTests.Interrupt.cs b/PanoramicData.Blazor.Test/Components/PDChatTests.Interrupt.cs index 9a094af21..f0c61cf60 100644 --- a/PanoramicData.Blazor.Test/Components/PDChatTests.Interrupt.cs +++ b/PanoramicData.Blazor.Test/Components/PDChatTests.Interrupt.cs @@ -131,7 +131,7 @@ public async Task Interrupting_leaves_the_text_box_unchanged() { var service = InterruptService(); var component = await RenderSpeakingAsync(service); - await component.Find("textarea").InputAsync(new ChangeEventArgs { Value = "A draft" }); + await component.InvokeAsync(() => component.Find("textarea").InputAsync(new ChangeEventArgs { Value = "A draft" })); await SayAsync(component, Merlin); await Task.Delay(_settle, Xunit.TestContext.Current.CancellationToken); diff --git a/PanoramicData.Blazor.Test/Components/PDContextMenuTests.Failures.cs b/PanoramicData.Blazor.Test/Components/PDContextMenuTests.Failures.cs new file mode 100644 index 000000000..538d9e1d0 --- /dev/null +++ b/PanoramicData.Blazor.Test/Components/PDContextMenuTests.Failures.cs @@ -0,0 +1,44 @@ +using AwesomeAssertions; +using Bunit; +using Microsoft.AspNetCore.Components.Web; +using Microsoft.JSInterop; + +namespace PanoramicData.Blazor.Test; + +///

+/// How contains failures: given an ExceptionHandler, a failure in showing the menu is passed +/// to it rather than ending the Blazor circuit, and without one it is thrown as before. +/// +public partial class PDContextMenuTests +{ + private static Task RightClickAsync(IRenderedComponent component) + => component.InvokeAsync(() => component.Find(".pdcontextmenuhost").MouseDownAsync(new MouseEventArgs { Button = 2 })); + + /// A failure in showing the menu, such as its script failing, is passed to ExceptionHandler. + [Fact] + public async Task AFailureShowingTheMenu_IsPassedToTheExceptionHandler() + { + _module.SetupVoid("showMenu", _ => true).SetException(new JSException("No menu")); + var failures = new List(); + var component = Render(parameters => parameters + .Add(p => p.Items, CreateItems()) + .Add(p => p.ExceptionHandler, failures.Add) + .AddChildContent("Right click me")); + + await RightClickAsync(component); + + failures.Should().ContainSingle().Which.Message.Should().Be("No menu"); + } + + /// Without an ExceptionHandler the failure is thrown, as it always was. + [Fact] + public async Task WithoutAnExceptionHandler_AFailureShowingTheMenu_IsThrownAsBefore() + { + _module.SetupVoid("showMenu", _ => true).SetException(new JSException("No menu")); + var component = RenderMenu(null); + + var act = () => RightClickAsync(component); + + await act.Should().ThrowAsync().WithMessage("No menu"); + } +} diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs index f6ba084f1..1553c1f65 100644 --- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs +++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Failures.cs @@ -92,4 +92,17 @@ public async Task NavigateToItemAsync_ALaterCall_SupersedesOneStillLoading() cut.Instance.SelectedFilesAndFolders.Should().Equal("/Media/pic.png"); cut.Instance.IsNavigating.Should().BeFalse(); } + + /// The tree's and table's right-click menus pass their failures on as the explorer does. + [Fact] + public void TheContextMenus_PassFailuresOn_OnlyWhenTheApplicationGivesAHandler() + { + var withHandler = RenderExplorer(); + var withoutHandler = Render(p => p.Add(x => x.DataProvider, _provider)); + + withHandler.FindComponents().Should().HaveCount(2) + .And.AllSatisfy(menu => menu.Instance.ExceptionHandler.HasDelegate.Should().BeTrue()); + withoutHandler.FindComponents().Should().HaveCount(2) + .And.AllSatisfy(menu => menu.Instance.ExceptionHandler.HasDelegate.Should().BeFalse()); + } } diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs index 23d6aa4e7..9bb5f71d7 100644 --- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs +++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Navigation.cs @@ -162,6 +162,74 @@ public async Task NavigateToItemAsync_NoItem_DoesNothing(string path) cut.Instance.FolderPath.Should().Be("/Docs"); } + /// Navigating to a folder opens it once, without opening the folders above it or first showing its parent. + [Fact] + public async Task NavigateToFolderAsync_OpensOnlyTheFolder() + { + _provider.Items.Remove(_provider.Get("/Docs/Sub")); + _provider.Items.AddRange( + [ + FileProvider.Dir("/Docs/Sub", hasSubFolders: true), + FileProvider.Dir("/Docs/Sub/Deep"), + FileProvider.File("/Docs/Sub/Deep/report.pdf", 100) + ]); + var folders = new List(); + var cut = RenderExplorer(p => p.Add(x => x.FolderChanged, (FileExplorerItem f) => folders.Add(f.Path))); + await NavigateAsync(cut, "/Media"); + folders.Clear(); + _provider.Requests.Clear(); + + var found = await cut.InvokeAsync(() => cut.Instance.NavigateToFolderAsync("/Docs/Sub/Deep")); + + found.Should().BeTrue(); + cut.Instance.FolderPath.Should().Be("/Docs/Sub/Deep"); + folders.Should().Equal("/Docs/Sub/Deep"); + _provider.Requests.Should().Equal("/Docs", "/Docs/Sub", "/Docs/Sub/Deep"); + Tree(cut).Instance.SelectedNode!.Key.Should().Be("/Docs/Sub/Deep"); + cut.Instance.SelectedFilesAndFolders.Should().BeEmpty(); + RowNames(cut).Should().Equal("..", "report.pdf"); + cut.Instance.IsNavigating.Should().BeFalse(); + } + + /// A missing folder on the way stops at the deepest folder found. + [Fact] + public async Task NavigateToFolderAsync_AMissingFolder_OpensTheDeepestFolderFound_AndReturnsFalse() + { + var cut = RenderExplorer(); + + var found = await cut.InvokeAsync(() => cut.Instance.NavigateToFolderAsync("/Docs/Gone")); + + found.Should().BeFalse(); + cut.Instance.FolderPath.Should().Be("/Docs"); + cut.Instance.IsNavigating.Should().BeFalse(); + } + + /// The root is a folder, so it can be opened. + [Fact] + public async Task NavigateToFolderAsync_TheRoot_OpensIt() + { + var cut = RenderExplorer(); + await NavigateAsync(cut, "/Docs"); + + var found = await cut.InvokeAsync(() => cut.Instance.NavigateToFolderAsync("/")); + + found.Should().BeTrue(); + cut.Instance.FolderPath.Should().Be("/"); + } + + /// A blank path is ignored. + [Fact] + public async Task NavigateToFolderAsync_ABlankPath_DoesNothing() + { + var cut = RenderExplorer(); + await NavigateAsync(cut, "/Docs"); + + var found = await cut.InvokeAsync(() => cut.Instance.NavigateToFolderAsync(" ")); + + found.Should().BeFalse(); + cut.Instance.FolderPath.Should().Be("/Docs"); + } + /// SelectedFilesAndFolders lists the selected paths, and SelectionChanged carries the selected items. [Fact] public async Task Selection_IsReportedToTheApplication() diff --git a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Preview.cs b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Preview.cs index a77cf6f88..aeb0ae963 100644 --- a/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Preview.cs +++ b/PanoramicData.Blazor.Test/Components/PDFileExplorerTests.Preview.cs @@ -120,4 +120,33 @@ public async Task PreviewKey_WhenNotOptional_DoesNothing() cut.Instance.PreviewPanelVisible.Should().BeTrue(); _splitter.Invocations["setSizes"].Should().BeEmpty(); } + + /// A hidden preview fetches nothing; shown again, it fetches the row chosen while it was hidden. + [Fact] + public async Task OptionalOn_AHiddenPreview_FetchesOnlyOnceShown() + { + var provider = new RecordingPreviewProvider(); + var cut = RenderExplorer(p => p + .Add(x => x.PreviewPanel, FilePreviewModes.OptionalOn) + .Add(x => x.PreviewProvider, provider)); + await ClickToolbarAsync(cut, "preview"); + provider.PreviewedPaths.Clear(); + + await SelectRowsAsync(cut, "/Docs"); + provider.PreviewedPaths.Should().BeEmpty(); + + await ClickToolbarAsync(cut, "preview"); + cut.WaitForAssertion(() => provider.PreviewedPaths.Should().Equal("/Docs")); + } + + private sealed class RecordingPreviewProvider : DefaultPreviewProvider + { + public List PreviewedPaths { get; } = []; + + public override Task GetPreviewInfoAsync(FileExplorerItem? item) + { + PreviewedPaths.Add(item?.Path); + return base.GetPreviewInfoAsync(item); + } + } } diff --git a/PanoramicData.Blazor.Test/Components/PDFilePreviewTests.cs b/PanoramicData.Blazor.Test/Components/PDFilePreviewTests.cs index 00090c27e..80f6b910f 100644 --- a/PanoramicData.Blazor.Test/Components/PDFilePreviewTests.cs +++ b/PanoramicData.Blazor.Test/Components/PDFilePreviewTests.cs @@ -153,6 +153,23 @@ public void A_failing_preview_falls_back_and_reports_the_exception() /// A preview provider returning a fixed result, recording which items are previewed and how often a spinner is asked for. /// Both timings are zero so the spinner path is decided by whether the preview task has completed. /// + /// Verifies that a hidden preview fetches nothing, and fetches the item chosen meanwhile once it shows. + [Fact] + public void A_hidden_preview_fetches_nothing_until_it_shows() + { + var provider = new FakePreviewProvider(new PreviewInfo { CssClass = "txt" }); + + var component = Render(parameters => parameters + .Add(p => p.PreviewProvider, provider) + .Add(p => p.IsVisible, false) + .Add(p => p.Item, _item)); + provider.PreviewedPaths.Should().BeEmpty(); + + component.Render(parameters => parameters.Add(p => p.IsVisible, true)); + + provider.PreviewedPaths.Should().Equal(_item.Path); + } + private sealed class FakePreviewProvider(Task preview) : IPreviewProvider { public FakePreviewProvider(PreviewInfo info) diff --git a/PanoramicData.Blazor/PDContextMenu.razor.cs b/PanoramicData.Blazor/PDContextMenu.razor.cs index 7c20f5306..344a92c6b 100644 --- a/PanoramicData.Blazor/PDContextMenu.razor.cs +++ b/PanoramicData.Blazor/PDContextMenu.razor.cs @@ -1,9 +1,15 @@ -namespace PanoramicData.Blazor; +using PanoramicData.Blazor.Helpers; + +namespace PanoramicData.Blazor; /// /// A Blazor component that displays a context menu when the user right-clicks on the wrapped content. /// -public partial class PDContextMenu +/// +/// When the application gives an , a failure in showing the menu or in an item's click, +/// such as its script failing, is passed to it instead of ending the Blazor circuit. +/// +public partial class PDContextMenu : IHandleEvent { private static int _idSequence; @@ -43,6 +49,18 @@ public partial class PDContextMenu /// [Parameter] public bool ShowOnMouseUp { get; set; } + /// + /// Gets or sets where a failure in one of the menu's events is passed. Without one, a failure is thrown as before. + /// + [Parameter] public EventCallback ExceptionHandler { get; set; } + + /// + /// Handles an event as does, passing a failure to when + /// the application gives one. + /// + Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg) + => ComponentExceptionGuard.HandleEventAsync(callback, arg, ExceptionHandler, StateHasChanged); + /// /// Gets the unique identifier of this panel. /// diff --git a/PanoramicData.Blazor/PDContextMenu.razor.css b/PanoramicData.Blazor/PDContextMenu.razor.css index 1ffdabd4a..c5a51590d 100644 --- a/PanoramicData.Blazor/PDContextMenu.razor.css +++ b/PanoramicData.Blazor/PDContextMenu.razor.css @@ -9,6 +9,7 @@ overflow: auto !important; } +/* Above what it opens over, such as a sticky table header: Bootstrap's drop-down level, unless the application sets its own */ .pdcontextmenu { -webkit-user-select: none; /* Chrome/Safari */ -moz-user-select: none; /* Firefox */ @@ -16,6 +17,7 @@ width: fit-content; flex-direction: column; display: none; + z-index: var(--pd-contextmenu-z-index, 1000); } .pdcontextmenu.show { diff --git a/PanoramicData.Blazor/PDContextMenu.razor.js b/PanoramicData.Blazor/PDContextMenu.razor.js index 7bb70d0f2..8cc48c34b 100644 --- a/PanoramicData.Blazor/PDContextMenu.razor.js +++ b/PanoramicData.Blazor/PDContextMenu.razor.js @@ -20,9 +20,11 @@ export function showMenu(menuId, x, y) { }; }, }; + // Fixed, so the menu opens at the pointer wherever it sits: under CSS zoom or a scaled parent, and in a panel that + // scrolls or clips. "positionFixed" was popper 1's option, which popper 2 ignores var options = { placement: "bottom-start", - positionFixed: true, + strategy: "fixed", }; menuEl.classList.add("show"); popper = window.Popper.createPopper(reference, menuEl, options); // this is popper v2.4.4 syntax diff --git a/PanoramicData.Blazor/PDFileExplorer.Operations.cs b/PanoramicData.Blazor/PDFileExplorer.Operations.cs index bcd50a8ae..08cd63a07 100644 --- a/PanoramicData.Blazor/PDFileExplorer.Operations.cs +++ b/PanoramicData.Blazor/PDFileExplorer.Operations.cs @@ -30,6 +30,9 @@ private async Task OnTogglePreviewPanelAsync() PreviewPanelVisible = !PreviewPanelVisible; await RefreshToolbarAsync(); + + // Shown again, the preview fetches the item chosen while it was hidden, also when a host toggles it + StateHasChanged(); } } diff --git a/PanoramicData.Blazor/PDFileExplorer.razor b/PanoramicData.Blazor/PDFileExplorer.razor index 296f5bb32..61f267a6f 100644 --- a/PanoramicData.Blazor/PDFileExplorer.razor +++ b/PanoramicData.Blazor/PDFileExplorer.razor @@ -130,6 +130,7 @@ @@ -224,6 +225,7 @@ @@ -424,6 +426,7 @@ diff --git a/PanoramicData.Blazor/PDFileExplorer.razor.cs b/PanoramicData.Blazor/PDFileExplorer.razor.cs index d4ad71375..ec7cc00d5 100644 --- a/PanoramicData.Blazor/PDFileExplorer.razor.cs +++ b/PanoramicData.Blazor/PDFileExplorer.razor.cs @@ -727,21 +727,42 @@ public async Task NavigateToItemAsync(string itemPath) IsNavigating = true; try { - var (folderNode, isFolderFound) = await LoadFolderNodesAsync(rootNode, segments[..^1], navigation).ConfigureAwait(true); - if (navigation != _itemNavigation) + return await OpenFolderAsync(rootNode, segments[..^1], navigation).ConfigureAwait(true) + && navigation == _itemNavigation + && await SelectTableItemAsync("/" + string.Join('/', segments)).ConfigureAwait(true); + } + finally + { + if (navigation == _itemNavigation) { - return false; + IsNavigating = false; } + } + } - if (Tree.SelectedNode != folderNode) - { - await Tree.SelectNode(folderNode, false).ConfigureAwait(true); - } + /// + /// Opens the given folder, as choosing it in the tree does. As with , the + /// folders above it are only loaded into the tree, so the table loads once and is raised + /// once, for the folder itself. + /// + /// The path of the folder to open, such as "/Docs/Sub"; "/" opens the root. + /// + /// True when the folder was found and opened. False when it was not, in which case the deepest folder found on the + /// way to it is open. + /// + public async Task NavigateToFolderAsync(string folderPath) + { + if (string.IsNullOrWhiteSpace(folderPath) || Tree?.RootNode.Find("/") is not { } rootNode) + { + return false; + } - await ScrollIntoViewAsync(() => Tree.ScrollNodeIntoViewAsync(folderNode)).ConfigureAwait(true); - return isFolderFound - && navigation == _itemNavigation - && await SelectTableItemAsync("/" + string.Join('/', segments)).ConfigureAwait(true); + var navigation = ++_itemNavigation; + IsNavigating = true; + try + { + return await OpenFolderAsync(rootNode, folderPath.Split('/', StringSplitOptions.RemoveEmptyEntries), navigation).ConfigureAwait(true) + && navigation == _itemNavigation; } finally { @@ -752,6 +773,27 @@ public async Task NavigateToItemAsync(string itemPath) } } + /// + /// Loads each folder on the way into the tree, then opens the deepest one found, and returns whether every folder + /// was found. Returns false without opening anything once a later navigation has started. + /// + private async Task OpenFolderAsync(TreeNode rootNode, string[] folderNames, int navigation) + { + var (folderNode, isFound) = await LoadFolderNodesAsync(rootNode, folderNames, navigation).ConfigureAwait(true); + if (navigation != _itemNavigation) + { + return false; + } + + if (Tree!.SelectedNode != folderNode) + { + await Tree.SelectNode(folderNode, false).ConfigureAwait(true); + } + + await ScrollIntoViewAsync(() => Tree.ScrollNodeIntoViewAsync(folderNode)).ConfigureAwait(true); + return isFound; + } + /// /// Scrolls something into view. Scrolling only helps the eye, so a browser that cannot do it, or a connection that /// has closed, is no reason to fail. diff --git a/PanoramicData.Blazor/PDFilePreview.razor.cs b/PanoramicData.Blazor/PDFilePreview.razor.cs index c1527f919..b099309ca 100644 --- a/PanoramicData.Blazor/PDFilePreview.razor.cs +++ b/PanoramicData.Blazor/PDFilePreview.razor.cs @@ -28,11 +28,18 @@ public partial class PDFilePreview [Parameter] public IPreviewProvider PreviewProvider { get; set; } = new DefaultPreviewProvider(); + /// + /// Gets or sets whether the preview is showing. While it is not, nothing is fetched; the item chosen meanwhile is + /// fetched once it shows. + /// + [Parameter] + public bool IsVisible { get; set; } = true; + /// protected override async Task OnParametersSetAsync() { // cache last preview by path - if (Item?.Path == _lastPreviewPath) + if (!IsVisible || Item?.Path == _lastPreviewPath) { return; }