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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user