Integrate DxPdfViewer and remove custom zoom controls

Replaced custom zoom controls in `EnvelopeReceiverPage.razor` with the built-in zoom functionality of the `DevExpress.Blazor.PdfViewer` component (`DxPdfViewer`).

- Removed custom zoom buttons, slider, and JavaScript zoom logic.
- Introduced `_viewerZoomLevel` to align with `DxPdfViewer.ZoomLevel`.
- Synchronized zoom state using `ZoomLevelChanged` to update `_currentZoom`.
- Updated overlay redraw logic to react to viewer zoom changes.
- Modified `FitToWidth` and `SetZoom` methods to work with `DxPdfViewer`.
- Updated documentation to reflect integration decisions and findings.

These changes simplify zoom handling, reduce UI redundancy, and ensure proper synchronization between the viewer and overlay logic.
This commit is contained in:
2026-06-29 14:08:51 +02:00
parent 1ac7188466
commit 03367ebc4a
3 changed files with 109 additions and 47 deletions

View File

@@ -223,27 +223,6 @@
</button> </button>
</div> </div>
<div class="pdf-toolbar__divider"></div>
<div class="pdf-toolbar__section pdf-toolbar__zoom-section">
<button class="pdf-toolbar__btn" @onclick="ZoomOut" disabled="@(_currentZoom <= 50)" title="Verkleinern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM4 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1H4z" />
</svg>
</button>
<div class="pdf-toolbar__zoom-slider-container">
<input type="range" class="pdf-toolbar__zoom-slider" min="50" max="300" step="@(PdfViewerOptions.Value.ZoomStepPercentage)" value="@_currentZoom" @oninput="OnZoomSliderChanged" title="@(_currentZoom)%" />
<div class="pdf-toolbar__zoom-label">@(_currentZoom)%</div>
</div>
<button class="pdf-toolbar__btn" @onclick="ZoomIn" disabled="@(_currentZoom >= 300)" title="Vergrößern">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM6.5 3a.5.5 0 0 0-1 0v2.5H3a.5.5 0 0 0 0 1h2.5V9a.5.5 0 0 0 1 0V6.5H9a.5.5 0 0 0 0-1H6.5V3z" />
</svg>
</button>
</div>
<div class="pdf-toolbar__divider"></div>
@if (_totalSignatures > 0) @if (_totalSignatures > 0)
{ {
<div class="pdf-toolbar__section"> <div class="pdf-toolbar__section">
@@ -351,9 +330,11 @@
<div id="pdf-dx-viewer-host" class="envelope-dx-viewer-host"> <div id="pdf-dx-viewer-host" class="envelope-dx-viewer-host">
@if (_pdfDocumentContent is not null && _pdfDocumentContent.Length > 0) @if (_pdfDocumentContent is not null && _pdfDocumentContent.Length > 0)
{ {
<DxPdfViewer CssClass="envelope-dx-pdf-viewer" <DxPdfViewer @ref="_pdfViewer"
CssClass="envelope-dx-pdf-viewer"
DocumentContent="@_pdfDocumentContent" DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_currentZoom" ZoomLevel="@_viewerZoomLevel"
ZoomLevelChanged="OnViewerZoomLevelChanged"
IsSinglePagePreview="true" /> IsSinglePagePreview="true" />
} }
<div id="pdf-signature-layer" class="pdf-signature-layer pdf-signature-layer--dx"></div> <div id="pdf-signature-layer" class="pdf-signature-layer pdf-signature-layer--dx"></div>
@@ -558,9 +539,11 @@
int _currentPage = 1; int _currentPage = 1;
int _totalPages = 0; int _totalPages = 0;
int _currentZoom = 150; int _currentZoom = 150;
double _viewerZoomLevel = 1.5;
bool _showThumbnails = true; bool _showThumbnails = true;
bool _isLoggingOut = false; bool _isLoggingOut = false;
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef; DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
DxPdfViewer? _pdfViewer;
IReadOnlyList<SignatureDto> _signatures = []; IReadOnlyList<SignatureDto> _signatures = [];
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver; EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
ClaimsPrincipal? _receiverUser; ClaimsPrincipal? _receiverUser;
@@ -766,6 +749,21 @@
await SetZoom(requestedZoom); await SetZoom(requestedZoom);
} }
async Task OnViewerZoomLevelChanged(double newZoomLevel)
{
_viewerZoomLevel = newZoomLevel;
if (newZoomLevel > 0)
{
_currentZoom = (int)Math.Round(newZoomLevel * 100, MidpointRounding.AwayFromZero);
}
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
await InvokeAsync(StateHasChanged);
}
async Task NextPage() async Task NextPage()
{ {
if (_currentPage >= _totalPages) if (_currentPage >= _totalPages)
@@ -799,7 +797,12 @@
async Task SetZoom(int percentage) async Task SetZoom(int percentage)
{ {
_currentZoom = Math.Clamp(percentage, 50, 300); _currentZoom = Math.Clamp(percentage, 50, 300);
await ApplyViewerStateAsync(); _viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
} }
async Task OnZoomSliderChanged(ChangeEventArgs e) async Task OnZoomSliderChanged(ChangeEventArgs e)
@@ -824,8 +827,13 @@
async Task FitToWidth() async Task FitToWidth()
{ {
_viewerZoomLevel = -2;
_currentZoom = 150; _currentZoom = 150;
await ApplyViewerStateAsync();
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150);
await RenderSignatureButtonsAsync();
} }
async Task ToggleThumbnails() async Task ToggleThumbnails()
@@ -1191,6 +1199,11 @@
if (!_pdfLoaded) if (!_pdfLoaded)
return; return;
if (_viewerZoomLevel > 0)
{
_viewerZoomLevel = _currentZoom / 100d;
}
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom); await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
await Task.Delay(150); await Task.Delay(150);

View File

@@ -78,28 +78,6 @@ window.pdfViewer = {
return; return;
} }
if (!this.wheelHandler) {
this.wheelHandler = async (e) => {
if (!(e.ctrlKey || e.metaKey) || !this.dotNetReference) {
return;
}
e.preventDefault();
const step = this.qualityOptions.zoomStepPercentage;
const nextZoom = e.deltaY < 0
? Math.min(this.currentZoom + step, 300)
: Math.max(this.currentZoom - step, 50);
if (nextZoom !== this.currentZoom) {
this.currentZoom = nextZoom;
await this.dotNetReference.invokeMethodAsync('OnZoomGestureRequested', nextZoom);
}
};
host.addEventListener('wheel', this.wheelHandler, { passive: false });
}
if (!this.resizeObserver && typeof ResizeObserver !== 'undefined') { if (!this.resizeObserver && typeof ResizeObserver !== 'undefined') {
this.resizeObserver = new ResizeObserver(() => this.requestOverlayRefresh()); this.resizeObserver = new ResizeObserver(() => this.requestOverlayRefresh());
this.resizeObserver.observe(host); this.resizeObserver.observe(host);

View File

@@ -939,6 +939,77 @@ That is the safest first vertical slice because:
- it reduces uncertainty in overlay scaling - it reduces uncertainty in overlay scaling
- it does not yet require full signature flow completion - it does not yet require full signature flow completion
### 9. Verified DevExpress API findings and current progress
The following points are now verified from DevExpress documentation for `DxPdfViewer`:
- `ZoomLevel` is a real component parameter
- positive `ZoomLevel` values are interpreted as percentages
- `ZoomLevelChanged` is supported and should be used to synchronize viewer zoom with page state
- `CustomizeToolbar` can clear all built-in toolbar items through `ToolbarModel.AllItems.Clear()`
- `ActivePageIndex` is read-only information and is not a page-navigation parameter
### 10. Recovery progress update
The first functional recovery step is zoom restoration.
Implemented direction:
- bind custom receiver toolbar zoom controls to `DxPdfViewer.ZoomLevel`
- synchronize zoom changes back into `_currentZoom` through `ZoomLevelChanged`
- remove competing custom JS ctrl+wheel zoom logic
- keep overlay redraw pipeline after confirmed zoom changes
Expected outcome after this step:
- toolbar zoom buttons visibly affect the DevExpress viewer
- zoom slider visibly affects the DevExpress viewer
- overlay refresh is triggered from real viewer zoom state instead of guessed zoom state
### 11. Additional findings after first zoom integration attempt
After the first live integration attempt against the installed `DevExpress.Blazor.PdfViewer` package version `25.2.3`, one important runtime behavior was confirmed:
- the `DxPdfViewer.ZoomLevel` value must be treated as a zoom factor for normal positive values in the live UI flow used here
- in practice, `1.5` corresponds to `150%`
- using `150` as the bound value causes the DevExpress viewer UI to display `15000%`
This means the receiver page must keep two different zoom representations:
- `_currentZoom` = receiver custom workflow percentage view, for example `150`
- `_viewerZoomLevel` = DevExpress viewer value, for example `1.5`
### 12. Current UI decision for zoom controls
The custom toolbar zoom section in `pdf-toolbar__zoom-section` is no longer considered desirable.
Current decision:
- remove the custom zoom buttons and slider from the receiver toolbar
- rely on the built-in DevExpress PDF Viewer zoom UI instead
- keep `ZoomLevelChanged` synchronization so overlay redraw logic can still react to viewer zoom changes
Reason:
- DevExpress already provides zoom UX
- duplicate zoom controls create confusing UX and unit mismatch risks
- the built-in viewer zoom UI is a better source of truth for the current zoom state
### 13. Current UI decision for thumbnails
The custom thumbnail sidebar is still kept for now.
Reason:
- current receiver workflow depends on custom thumbnail shell behavior
- width persistence and resizable splitter are already implemented in the custom sidebar
- no verified built-in `DxPdfViewer` thumbnail sidebar integration surface has yet been confirmed from the currently inspected API surface
So the current decision is:
- keep custom thumbnail sidebar for now
- revisit possible DevExpress-native thumbnail navigation later only if it supports the required receiver workflow behavior
--- ---
## Files Most Likely Relevant For Future Work ## Files Most Likely Relevant For Future Work