Migrate PDF.js to DevExpress DxPdfViewer

Transitioned PDF rendering in `EnvelopeReceiverPage.razor`
from `PDF.js` to `DevExpress DxPdfViewer`. Updated code
and documentation to reflect the verified API of
`DevExpress.Blazor.PdfViewer` v25.2.3, addressing its
limitations (e.g., lack of `GoToPageAsync`, `PageNumberChanged`).

Implemented `CustomizeToolbar` for navigation/zoom controls
and manual state tracking for `_currentPage` and `_viewerZoomLevel`.
Replaced JavaScript interop for page count with the `PageCount`
property. Retained the custom thumbnail sidebar due to API
constraints.

Added temporary debug tools for DOM analysis and navigation
testing. Updated `TESTING_CHECKLIST.md` and added
`DEVEXPRESS_V25_LIMITATIONS.md` to document the new strategy,
API limitations, and testing scenarios. Cross-page signature
navigation implemented with state updates, though visible
page changes remain manual.

Prepared for future improvements while ensuring functional
migration to `DxPdfViewer`.
This commit is contained in:
2026-06-30 16:12:05 +02:00
parent a10ee590c9
commit 99fbb33f1c
5 changed files with 1009 additions and 23 deletions

View File

@@ -64,13 +64,96 @@ This means `DxPdfViewer` must become the main document rendering surface without
- signature navigation across pages
- overlay repositioning and resizing after zoom/page changes
### ? CRITICAL: DevExpress Integration Strategy
**Current implementation uses DevExpress built-in toolbar with C# API event handling.**
**?? DevExpress v25.2.3 Limitation: No `ToolbarVisible` property exists!**
- Toolbar cannot be completely hidden
- Use `CustomizeToolbar` event to minimize toolbar items
**Primary control method:**
- DevExpress `DxPdfViewer` component with `CustomizeToolbar` event
- Toolbar customized to show only essential navigation/zoom controls
- Custom signature-specific toolbar preserved separately
**C# API Integration:**
1. **Toolbar Customization** ? Minimize DevExpress toolbar:
```csharp
<DxPdfViewer CustomizeToolbar="OnCustomizeToolbar" />
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
// Keep only essential items
var essentialItems = toolbarModel.AllItems
.Where(item => item.Id == ToolbarItemId.PreviousPage ||
item.Id == ToolbarItemId.NextPage ||
item.Id == ToolbarItemId.ZoomIn ||
item.Id == ToolbarItemId.ZoomOut)
.ToList();
toolbarModel.AllItems.Clear();
foreach (var item in essentialItems)
toolbarModel.AllItems.Add(item);
}
```
2. **Page Navigation Events** ? React to DevExpress navigation:
```csharp
<DxPdfViewer PageNumberChanged="OnPageNumberChanged" />
private async Task OnPageNumberChanged(int newPageNumber)
{
_currentPage = newPageNumber;
await RenderSignatureButtonsAsync(); // Update overlays
}
```
3. **Zoom Events** ? React to DevExpress zoom:
```csharp
<DxPdfViewer ZoomLevelChanged="OnZoomLevelChanged" />
private async Task OnZoomLevelChanged(double newZoomLevel)
{
_viewerZoomLevel = newZoomLevel;
_currentZoom = (int)Math.Round(newZoomLevel * 100);
await RenderSignatureButtonsAsync(); // Update overlays
}
```
**JavaScript role (LIMITED):**
- ? Overlay geometry calculations (signature placeholder positioning)
- ? PDF.js helper for thumbnail generation
- ? Custom UI interactions (sidebar resize, signature canvas)
- ? Scroll-to-element helper for signature navigation
**JavaScript MUST NOT:**
- ? Attempt to control DxPdfViewer page navigation via DOM manipulation
- ? Attempt to control DxPdfViewer zoom via DOM manipulation
- ? Use jQuery/DevExtreme client API to control the component
**IMPORTANT: CustomizeToolbar Event**
- DevExpress `CustomizeToolbar` event is the ONLY way to modify toolbar in v25.2.3
- Cannot hide toolbar completely - can only customize items
- See: https://docs.devexpress.com/Blazor/DevExpress.Blazor.PdfViewer.DxPdfViewer.CustomizeToolbar
**Reference:**
- Official API: https://docs.devexpress.com/Blazor/DevExpress.Blazor.PdfViewer.DxPdfViewer
- DevExpress Documentation MCP Server: https://docs.devexpress.com/GeneralInformation/405551/help-resources/dev-express-documentation-mcp-server-configure-an-ai-powered-assistant
### 3. Custom enhancement layer
Extra behavior is implemented through custom JavaScript and CSS:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
These files extend the base PDF.js experience with receiver-specific features.
These files extend the PDF viewer experience with receiver-specific features.
**Important:** After `DxPdfViewer` migration, `pdf-viewer.js` role changes:
- **BEFORE migration:** Full PDF.js control (page nav, zoom, rendering)
- **AFTER migration:** Limited to overlay geometry and PDF.js thumbnail helper only
- **DxPdfViewer control:** Exclusively through C# API in `EnvelopeReceiverPage.razor`
---
@@ -943,10 +1026,45 @@ That is the safest first vertical slice because:
The following points are now verified for the currently installed `DevExpress.Blazor.PdfViewer` package version `25.2.3`:
- `ZoomLevel` is a real component parameter
- normal positive zoom values used in this receiver page must be treated as factors in the live UI flow here (`1.5` = `150%`)
- `ActivePageIndex` is read-only information and is not a page-navigation parameter
- `ZoomLevelChanged` must not be used in this workspace because the current installed component surface does not expose a matching incoming parameter on `DxPdfViewer`
**Available `[Parameter]` properties (GET/SET):**
- `DocumentContent` (`byte[]`) — feed PDF as byte array ?
- `ZoomLevel` (`double`) — zoom factor, not percentage. `1.5` = `150%` ?
- `IsSinglePagePreview` (`bool`) — single page mode ?
- `CssClass` (`string`) — assign CSS class ?
- `DocumentName` (`string`) — download filename, default `"Document"` ?
- `SizeMode` (`SizeMode?`) — `Small`, `Medium`, `Large` ?
**Read-only properties (GET only, no setter):**
- `ActivePageIndex` (`int`) — active page index, 0-based. Cannot SET, no programmatic navigation. ?
- `PageCount` (`int`) — total pages in document. **Replaces JS `getTotalPages()` call.** ?
**Available event:**
- `CustomizeToolbar` — only event available for toolbar customization ?
**Does NOT exist in v25.2.3:**
- `PageNumberChanged` event ?
- `ZoomLevelChanged` event ?
- `ToolbarVisible` property ?
- `GoToPageAsync()` method ?
- `GoToNextPageAsync()` method ?
- `ZoomAsync()` method ?
**Critical ZoomLevel rule:**
- ZoomLevel is a **factor**, not a percentage
- `_viewerZoomLevel = _currentZoom / 100d` — always divide by 100
- Example: `_currentZoom = 150` ? `_viewerZoomLevel = 1.5`
- Using `150` directly causes DevExpress to display `15000%`
**PageCount usage (no JS needed):**
```csharp
// In OnAfterRenderAsync
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
{
_totalPages = _pdfViewer.PageCount; // direct, no JS interop
_pdfLoaded = true;
await InvokeAsync(StateHasChanged);
}
```
### 10. Recovery progress update
@@ -977,36 +1095,142 @@ 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
### 12. FINAL DECISION: CustomizeToolbar + Manual State Tracking
The custom toolbar zoom section in `pdf-toolbar__zoom-section` is no longer considered desirable.
**Implementation strategy (verified for v25.2.3):**
Current decision:
1. **Remove custom PDF toolbar** (page navigation, zoom controls from our HTML)
- DevExpress provides these via `CustomizeToolbar` event
- Add custom prev/next/zoom-in/zoom-out as `ToolbarItem` objects
- Each button manually updates `_currentPage`, `_viewerZoomLevel`, then calls `RenderSignatureButtonsAsync()`
- 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
2. **Preserve signature-specific toolbar** (separate from DxPdfViewer toolbar)
- Signature change button
- Previous/Next signature navigation
- Signature counter (signed/unsigned/total)
- Reset button
Reason:
3. **Correct DxPdfViewer usage:**
- 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
```razor
<DxPdfViewer @ref="_pdfViewer"
CssClass="envelope-dx-pdf-viewer"
DocumentContent="@_pdfDocumentContent"
ZoomLevel="@_viewerZoomLevel"
IsSinglePagePreview="true"
CustomizeToolbar="OnCustomizeToolbar" />
```
```csharp
// Two zoom representations required:
// _currentZoom = 150 (UI display: "150%")
// _viewerZoomLevel = 1.5 (DxPdfViewer parameter: factor)
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
{
toolbarModel.AllItems.Clear();
var prevButton = new ToolbarItem
{
IconCssClass = "dx-icon-chevronprev",
Enabled = _currentPage > 1,
Click = async (args) =>
{
if (_currentPage > 1)
{
_currentPage--;
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync();
}
}
};
// ... add nextButton, zoomIn, zoomOut similarly
toolbarModel.AllItems.Add(prevButton);
}
```
**Why this is the correct approach for v25.2.3:**
- `GoToPageAsync()` does NOT exist ?
- `PageNumberChanged` event does NOT exist ?
- `ZoomLevelChanged` event does NOT exist ?
- `ToolbarVisible` property does NOT exist ?
- `CustomizeToolbar` is the ONLY available hook ?
- `ZoomLevel` binding works but needs factor format (divide by 100) ?
- `PageCount` property is available directly ?
### 13. Current UI decision for thumbnails
The custom thumbnail sidebar is still kept for now.
**Decision: Keep custom thumbnail sidebar**
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
- Current receiver workflow depends on custom thumbnail shell behavior
- Width persistence and resizable splitter are already implemented in the custom sidebar
- Thumbnail click updates `_currentPage` state and calls `RenderSignatureButtonsAsync()`
- **Known limitation:** thumbnail click cannot move DevExpress viewer to target page (no `GoToPageAsync()` in v25.2.3)
- Active thumbnail highlight follows `_currentPage` state
So the current decision is:
**What thumbnail click can do:**
```csharp
async Task GoToPageFromThumbnail(int pageNum)
{
if (pageNum < 1 || pageNum > _totalPages) return;
_currentPage = pageNum; // state updated
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
await RenderSignatureButtonsAsync(); // overlays refreshed for new page
// NOTE: DxPdfViewer visible page does NOT change - v25.2.3 has no navigation API
}
```
- keep custom thumbnail sidebar for now
- revisit possible DevExpress-native thumbnail navigation later only if it supports the required receiver workflow behavior
**Future consideration:**
- Evaluate DevExpress `ThumbnailPanelVisible` property if it becomes available
- Full thumbnail navigation requires v25.2.3 API upgrade or alternative approach
### 14. Signature Navigation Implementation
**Cross-page signature navigation limitation in v25.2.3:**
Because `GoToPageAsync()` does NOT exist, cross-page signature navigation is limited.
The current workaround updates `_currentPage` state and refreshes overlays, but the
DxPdfViewer visible page does not programmatically change.
```csharp
private async Task GoToNextSignature()
{
// Find next signature across all pages
var nextSig = FindNextSignatureFromCurrent();
if (nextSig == null) return;
if (nextSig.PageNumber != _currentPage)
{
// Update state - overlays will refresh for new page
// NOTE: DxPdfViewer visible page does NOT change (no GoToPageAsync in v25.2.3)
// The user must use the custom toolbar prev/next buttons to navigate pages
_currentPage = nextSig.PageNumber;
_viewerZoomLevel = _currentZoom / 100d;
await InvokeAsync(StateHasChanged);
}
// Refresh overlays for current page
await RenderSignatureButtonsAsync();
await UpdateSignatureCounterAsync();
// Scroll to signature element if on same page (JS helper for UI only)
await JSRuntime.InvokeVoidAsync("pdfViewer.scrollToSignature", nextSig.Id);
}
```
**Why GoToPageAsync is not available:**
- `GoToPageAsync()` does NOT exist in v25.2.3 ?
- `PageNumberChanged` event does NOT exist to confirm navigation ?
- Only `CustomizeToolbar` buttons can trigger verifiable page state changes ?
**Acceptable workaround:**
- Custom toolbar prev/next buttons are the only reliable navigation mechanism
- Signature navigation updates state and refreshes overlays
- Users navigate to the correct page using toolbar buttons when cross-page jump is needed
---