Files
EnvelopeGenerator/RECEIVER_PDF_VIEWER_CONTEXT.md
TekH 99fbb33f1c 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`.
2026-06-30 16:12:05 +02:00

1273 lines
40 KiB
Markdown

# EnvelopeGenerator — Receiver PDF Viewer Context
## Purpose
This document summarizes the active receiver-side PDF viewing and signing experience so that other agents can understand the current implementation quickly without re-reading all related files.
This summary is based on the active host and current implementation in:
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
- `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`
---
## Active Page
**Primary receiver page:**
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
**Route:**
- `/envelope/{EnvelopeKey}`
**Render mode:**
- `InteractiveServer`
This page is the active receiver PDF viewing and signing UI.
---
## Core Architecture
The receiver experience is built from three layers:
### 1. Blazor page layer
`EnvelopeReceiverPage.razor` is responsible for:
- authorization flow
- loading receiver-specific document data
- loading signature placeholders
- loading and saving cached signature data
- UI state management
- toolbar interactions
- popup interactions
- JS interop orchestration
### 2. PDF rendering layer
The active implementation currently uses `PDF.js` for:
- rendering the active PDF page
- rendering thumbnails
- handling zoom and page navigation state
Referenced assets in the current implementation:
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
### Planned target rendering layer
The main migration goal is to replace `PDF.js` in `EnvelopeReceiverPage.razor` with `DxPdfViewer` while preserving the complete receiver signing experience documented in this file.
This means `DxPdfViewer` must become the main document rendering surface without regressing:
- single active page viewing behavior
- page navigation behavior
- zoom behavior
- thumbnail sidebar behavior
- receiver-specific signature placeholder overlays
- applied signature overlays
- 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 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`
---
## Main Functional Capabilities
## 1. Single-page PDF viewing in the main viewer
The page shows one active PDF page at a time in the main canvas.
Main viewer elements:
- `pdf-canvas`
- `pdf-text-layer`
- `pdf-signature-layer`
This means the page is not using a continuous full-document scroll layout in the main area. Instead, it behaves like a single active page viewer with controlled navigation.
---
## 2. Page navigation
The page supports multiple navigation methods:
- previous page button
- next page button
- direct page number input
- thumbnail click navigation
- signature navigation that may jump to another page automatically
Relevant Blazor methods include:
- `PreviousPage()`
- `NextPage()`
- `OnPageInputChanged(...)`
- `GoToPageFromThumbnail(...)`
- `OnPageChangedBySignatureNav(...)`
---
## 3. Zoom support
The page supports zooming in and out for the active PDF page.
Available zoom behavior:
- zoom in button
- zoom out button
- zoom slider
- programmatic scale setting
- fit-to-width helper exists in code
Relevant methods:
- `ZoomIn()`
- `ZoomOut()`
- `SetZoom(int percentage)`
- `OnZoomSliderChanged(...)`
- `FitToWidth()`
- `OnZoomChanged(double scale)`
Current zoom constraints in the page:
- minimum: `50%`
- maximum: `300%`
Important behavior:
- when zoom changes, signature placeholders and applied signature overlays are re-rendered/repositioned
- position and visual size are expected to stay synchronized with the current PDF scale
---
## 4. Thumbnail sidebar
The page has a thumbnail sidebar for document page previews.
Supported sidebar behavior:
- show/hide toggle
- click thumbnail to navigate to page
- highlight active page
- resizable width
- width persistence in `localStorage`
Relevant state:
- `_showThumbnails`
- `_thumbnailWidth`
- `_isResizing`
Relevant methods:
- `ToggleThumbnails()`
- `RenderThumbnailsAsync()`
- `OnSplitterMouseDown(...)`
- `OnSplitterMouseMove(...)`
- `OnSplitterMouseUp()`
Persistence key:
- `envelopeViewer_thumbnailWidth`
Important implementation detail:
- thumbnail rendering is done sequentially with delay to avoid overloading the browser
---
## 5. Signature placeholder buttons on top of the PDF
The page places clickable signature buttons over the PDF for receiver-specific signature fields.
Behavior:
- placeholders are loaded server-side
- placeholders are rendered on the client as overlay elements
- placeholders are shown for the current page
- clicking a placeholder applies the captured signature to that location
Relevant Blazor method:
- `RenderSignatureButtonsAsync()`
Relevant JS interop call:
- `pdfViewer.renderSignatureButtons`
Important note:
- this is an overlay workflow, not direct PDF stamping
- placeholders belong to the current authenticated receiver
---
## 6. Applying a real signature after clicking a placeholder
Once a receiver has a captured signature, clicking a signature placeholder applies the real signature overlay to the PDF viewer.
Applied signature data includes:
- signature image data URL
- signer full name
- signer position
- place
Relevant method:
- `OnSignatureButtonClick(int signatureId)`
Relevant JS interop call:
- `pdfViewer.applySignature`
Important note:
- the active implementation visually places the signature in the viewer layer
- the page is not currently doing final PDF binary stamping in this UI flow
---
## 7. Signature navigation across all signature fields
The toolbar supports fast navigation between signature fields.
Supported behavior:
- go to previous signature
- go to next signature
- maintain current signature index
- show signed / unsigned / total counts
- automatically move between pages if the next signature is on another page
Relevant methods:
- `GoToPreviousSignature()`
- `GoToNextSignature()`
- `OnSignatureNavChanged()`
- `OnPageChangedBySignatureNav(int newPage)`
- `UpdateSignatureCounterAsync()`
Relevant JS interop calls:
- `pdfViewer.goToPreviousSignature`
- `pdfViewer.goToNextSignature`
- `pdfViewer.getSignatureNavState`
Counter state maintained in Blazor:
- `_totalSignatures`
- `_signedSignatures`
- `_unsignedSignatures`
- `_currentSignatureIndex`
---
## 8. Automatic signature/button repositioning and resizing on zoom
A critical current feature is that zoom changes should keep overlay elements visually aligned with the document.
This applies to:
- signature placeholder buttons
- applied signature overlays
Expected behavior:
- position updates when zoom changes
- size updates when zoom changes
- re-render happens after page change and zoom change
This behavior is triggered from methods such as:
- `OnZoomChanged(...)`
- `ZoomIn()`
- `ZoomOut()`
- `OnZoomSliderChanged(...)`
- `NextPage()`
- `PreviousPage()`
- `GoToPageFromThumbnail(...)`
- `OnPageChangedBySignatureNav(...)`
In practice, the page repeatedly calls:
- `RenderSignatureButtonsAsync()`
This is one of the key behaviors other agents must preserve.
---
## 9. Signature capture popup
Signature creation is handled with a `DxPopup`.
Popup capabilities:
- draw signature
- create typed signature
- upload signature image
- capture required signer metadata
- validate required fields before save
Tabs:
- `draw`
- `text`
- `image`
Relevant constants:
- `SignatureTabDraw`
- `SignatureTabText`
- `SignatureTabImage`
Relevant canvas/input IDs:
- `envelope-signature-pad`
- `envelope-typed-signature-pad`
- `envelope-signature-image-input`
- `envelope-image-signature-pad`
---
## 10. Signature capture modes
The current UI supports three signature creation modes.
### Draw mode
Receiver signs directly on a canvas.
Relevant JS usage:
- `receiverSignature.initialize`
- `receiverSignature.clear`
- `receiverSignature.getDataUrl`
- `receiverSignature.loadExistingSignature`
### Text mode
Receiver enters a signature as text and chooses a font.
Relevant JS usage:
- `receiverSignature.initializeTyped`
- `receiverSignature.renderTypedSignature`
- `receiverSignature.clearTyped`
- `receiverSignature.getTypedDataUrl`
### Image mode
Receiver uploads an image of their signature.
Relevant JS usage:
- `receiverSignature.initializeImage`
- `receiverSignature.clearImage`
- `receiverSignature.getImageDataUrl`
---
## 11. Signature metadata requirements
The popup captures extra metadata besides the signature image.
### Required
- full name
- place
### Optional
- position
Relevant bound fields:
- `_signerFullName`
- `_signaturePlace`
- `_signerPosition`
Validation behavior in `SaveSignatureAsync()`:
- full name must not be empty
- place must not be empty
- signature image/data must not be empty
---
## 12. Cached signature reuse
The page attempts to load a previously cached signature for the receiver.
Behavior:
- if cache exists, the popup does not open automatically
- if cache does not exist, the popup opens on initial load
- saving a signature stores it back through the page data service
Relevant service usage:
- `PageDataService.GetCachedSignatureAsync(...)`
- `PageDataService.SaveCachedSignatureAsync(...)`
Related state:
- `_capturedSignature`
- `_signaturePopupVisible`
Important note:
- the active cache handling is server-side through distributed cache
- the receiver signature is not only in browser state
---
## 13. Signature change locking after signing starts
The current page prevents changing the captured signature after at least one signature has already been applied.
Behavior:
- signature change button becomes disabled when `_signedSignatures > 0`
- title explains the signature is locked
- reset requires restarting the signing session
Relevant methods:
- `GetSignatureButtonTitle()`
- `HandleSignatureChangeClick()`
- `RestartSigning()`
Important implication:
- once actual field signing begins, the selected/captured signature is treated as fixed for that session unless the page is reset
---
## 14. Reset/restart signing flow
The page includes a reset/restart behavior.
Current behavior:
- page reload is used to reset signing UI state
- this clears current in-view applied signatures and session UI state
Relevant method:
- `RestartSigning()`
Implementation detail:
- current reset behavior is based on `Navigation.NavigateTo(Navigation.Uri, forceLoad: true)`
---
## 15. PDF quality and rendering options
The page sends rendering options from .NET to JavaScript before viewer initialization.
Relevant options include:
- thumbnail base scale
- thumbnail HiDPI support
- thumbnail max DPR
- main canvas HiDPI support
- main canvas max DPR
- smooth zoom enablement
- zoom transition duration
- rendering opacity
- zoom step percentage
These come from:
- `IOptions<PdfViewerOptions>`
Relevant JS call:
- `pdfViewer.setQualityOptions(...)`
This means rendering quality and viewer performance are configurable from server-side options.
---
## 16. Local storage usage
The page currently uses `localStorage` at least for UI preferences.
Known usage:
- thumbnail sidebar width persistence
Known key:
- `envelopeViewer_thumbnailWidth`
This is UI preference storage, not the main signature cache mechanism.
---
## 17. Receiver authorization dependency
The page is not a public PDF viewer. It depends on receiver-specific authorization.
Before loading the document:
- `ReceiverAuthorizationService.AuthorizeAsync(EnvelopeKey)` is executed
- unauthorized users are redirected to `/envelope/login/{EnvelopeKey}`
Implication for other agents:
- viewer behavior is tied to authenticated receiver context
- placeholder loading and cached signature loading are receiver-specific
---
## 18. Receiver-specific data loading
The page loads its data through `EnvelopeReceiverPageDataService`.
Main server-side data loaded:
- document bytes
- signature placeholders
- receiver envelope data
- cached signature
Important domain behavior:
- signature placeholders are filtered for the authenticated receiver
- placeholder coordinates are converted to points before UI use
This matters for any future changes involving coordinate systems or overlay placement.
---
## Important Non-Goals / Current Constraints
### Not acceptable as a migration outcome
The migration is **not** successful if it results in:
- losing any receiver-side behavior documented in this file
- replacing the current workflow with a simplified read-only PDF viewer
- removing signature placeholder overlays or applied signature overlays
- removing cross-page signature navigation
- breaking zoom-time overlay synchronization
- falling back to legacy `ReceiverUI` as the main implementation target
- changing the flow into direct PDF binary stamping-only behavior in the main receiver UI
### Current active model
The current implementation **is** based on:
- `EnvelopeGenerator.Server`
- `EnvelopeReceiverPage.razor`
- `PDF.js`
- custom overlay and JS interop behavior
### Target model
The intended target model is:
- `EnvelopeGenerator.Server`
- `EnvelopeReceiverPage.razor`
- `DxPdfViewer` as the main PDF rendering component
- equivalent custom behavior for overlays, navigation, zoom synchronization, and receiver signing interactions
- preservation of the current receiver-specific authorization and page data loading flow
---
## Key Behaviors Other Agents Must Preserve
If another agent modifies this area, these behaviors are important to keep intact:
1. Receiver authorization before document access
2. Single active page PDF viewing
3. Page navigation and thumbnail navigation
4. Zoom in/out with correct overlay synchronization
5. Signature placeholder rendering on the correct page
6. Clicking a placeholder applies the captured signature overlay
7. Fast previous/next signature navigation across pages
8. Automatic overlay position and size updates after zoom/page changes
9. Signature popup with draw/text/image modes
10. Required metadata validation for full name and place
11. Cached signature reuse
12. Signature lock after signing has started
13. Reset/restart signing behavior
14. Thumbnail width persistence
## Migration Requirement
Any migration from `PDF.js` to `DxPdfViewer` in `EnvelopeReceiverPage.razor` must be treated as a rendering engine swap, not a workflow redesign.
The expected outcome is:
- `DxPdfViewer` replaces `PDF.js` as the main viewer technology
- the receiver authorization model remains unchanged
- the existing signature capture popup model remains unchanged
- signature placeholder rendering remains receiver-specific
- applied signature visuals remain aligned with the rendered document
- page navigation, zoom, thumbnails, and signature navigation continue to behave equivalently
- no documented feature in this file is dropped during the migration
---
## Suggested Mental Model
A useful way to think about the page is:
- `EnvelopeReceiverPage.razor` = orchestration and state
- current rendering engine = `PDF.js`
- target rendering engine = `DxPdfViewer`
- current viewer behavior layer = `pdf-viewer.js` for overlays, navigation, thumbnails, resize logic
- `receiver-signature.js` = signature capture/creation logic
- server page data service = receiver-specific document and signature source
---
## Current Post-Migration Status and Step-by-Step Feature Recovery Plan
This section replaces the earlier generic migration plan and records the current real status after the first `DxPdfViewer` swap attempt.
### 1. Current status summary
The main document is now visible with `DxPdfViewer`, which means:
- receiver authorization still works
- document loading still works
- the route and page host still work
- `DxPdfViewer` is now the visible main rendering surface
However, the receiver experience is currently **feature-incomplete**.
At the moment, the page is effectively in this state:
- PDF is displayed
- custom workflow shell still exists
- but the old `PDF.js` behavior contract is no longer fully connected to the new viewer
So this is no longer a rendering failure.
It is now a **behavior recovery** problem.
### 2. What is currently missing or unreliable
The following features are currently missing, incomplete, or highly likely to be non-functional in the present state.
#### 2.1 Page navigation is not truly connected to `DxPdfViewer`
Symptoms:
- previous page button may update Blazor state only
- next page button may update Blazor state only
- direct page input may update Blazor state only
- thumbnail click may update Blazor state only
- signature navigation page jumps may update Blazor state only
Why:
- the earlier attempt tried to use `ActivePageIndex`, but `DxPdfViewer` does not expose it as a component parameter in this version
- after removing that invalid parameter, the page still updates `_currentPage`, but that does not automatically move the DevExpress viewer to another page
- in other words, there is currently no verified programmatic page-navigation bridge into `DxPdfViewer`
Impact:
- all features that depend on true page switching are blocked or only partially simulated
#### 2.2 Zoom controls are not truly connected to `DxPdfViewer`
Symptoms:
- zoom in/out buttons may update `_currentZoom`
- slider may update `_currentZoom`
- ctrl+wheel logic may update `_currentZoom`
- but the visible viewer may not actually zoom accordingly, or may not do so reliably
Why:
- `ZoomLevel` is exposed as a property on `DxPdfViewer`, but it is not yet confirmed that changing the bound value gives the same runtime behavior expected by the old `PDF.js` flow
- the current implementation updates Blazor state and triggers overlay refresh, but the real DevExpress zoom lifecycle and its timing have not yet been validated
- no verified zoom-changed callback from `DxPdfViewer` has been integrated
Impact:
- zoom UX is incomplete
- overlay synchronization cannot be trusted until real zoom behavior is confirmed
#### 2.3 Overlay positioning is heuristic, not yet proven
Symptoms:
- signature placeholders may not appear
- or may appear in the wrong location
- or may not track zoom/page changes correctly
Why:
- old implementation owned the page canvas directly
- new implementation tries to detect the visible page surface inside `DxPdfViewer` DOM heuristically
- this means the current adapter is guessing which DOM element is the active rendered page surface
- page geometry, offsets, scaling and scroll surface are not yet bound to a stable DevExpress contract
Impact:
- placeholder buttons are not yet production-safe
- applied signatures are not yet production-safe
#### 2.4 Applied signature overlay behavior is not yet dependable
Symptoms:
- clicking a placeholder may not place the visual signature correctly
- applied signatures may drift after zoom
- applied signatures may disappear or misalign when navigation changes
Why:
- applied signatures currently depend on the same unverified geometry bridge as placeholders
- they also depend on reliable page identity and zoom identity from the DevExpress surface
Impact:
- the core signing UX is not yet restored
#### 2.5 Signature navigation is structurally present but functionally blocked
Symptoms:
- previous/next signature buttons may update internal navigation state
- but page-jump and scroll-to-target behavior may fail or be inconsistent
Why:
- signature navigation depends on:
- real page navigation
- real overlay rendering
- reliable scroll container discovery
- since all three are not yet stabilized, signature navigation cannot yet be trusted
Impact:
- navigation across signature fields is not complete
#### 2.6 Thumbnail sidebar is only partially preserved
What still exists:
- sidebar shell
- width persistence
- resize logic
- thumbnail rendering through helper pipeline
What is missing or uncertain:
- guaranteed synchronization between thumbnail click and visible page in `DxPdfViewer`
- guaranteed active-page highlight based on real viewer page
Why:
- active thumbnail currently follows `_currentPage`
- but `_currentPage` is not yet guaranteed to reflect the actual active page inside `DxPdfViewer`
#### 2.7 Single-page behavior is not yet guaranteed to match the old experience
Why:
- old implementation enforced a single active rendered page in a fully custom surface
- new implementation uses `DxPdfViewer IsSinglePagePreview="true"`
- but this still needs runtime verification against:
- page scroll behavior
- zoom behavior
- page switch timing
- overlay surface alignment
### 3. Root cause analysis: why features disappeared
The features disappeared for one main reason:
- the old receiver experience was not just a PDF viewer
- it was a **custom stateful viewer platform** built on top of `PDF.js`
In the old system:
- page navigation was owned by our JavaScript
- zoom was owned by our JavaScript
- page metrics were owned by our JavaScript
- active page surface was owned by our JavaScript
- overlays were drawn on DOM elements owned by our JavaScript
After the swap:
- the visible rendering surface belongs to DevExpress
- but the custom behavior layer still expects low-level ownership similar to `PDF.js`
So the missing features are not random regressions.
They are the direct result of replacing the renderer before fully rebuilding the viewer behavior bridge.
### 4. New recommended implementation strategy
Do **not** try to restore everything at once.
The correct path now is to restore behavior in controlled stages, one capability at a time.
### 5. Step-by-step recovery plan
#### Step 1 — Confirm the real `DxPdfViewer` API surface
Goal:
- stop guessing which `DxPdfViewer` properties and behaviors are actually bindable and runtime-reactive
Tasks:
- inspect the actual supported parameter surface for the installed DevExpress version
- determine which of the following are truly supported and reactive:
- zoom binding
- page navigation binding
- single-page mode behavior
- toolbar customization / suppression
- determine whether DevExpress exposes any JS/API for page switching or current page retrieval
Expected output:
- a verified list of what `DxPdfViewer` can really do directly
- a list of behaviors that must remain custom because DevExpress does not expose them
This step is mandatory before further feature work.
#### Step 2 — Restore real zoom behavior first
Goal:
- make zoom in/out/slider visibly affect `DxPdfViewer`
Why first:
- zoom is simpler than full page navigation
- it is also required before overlay sizing can be trusted
Tasks:
- verify whether `ZoomLevel` binding actually updates the viewer at runtime
- if yes, stabilize it
- if no, find the supported DevExpress-side mechanism
- disable or adjust any overlay refresh logic until true zoom behavior is confirmed
Acceptance for this step:
- zoom in button visibly zooms document
- zoom out button visibly zooms document
- slider visibly changes viewer zoom
- `_currentZoom` matches what the user sees
#### Step 3 — Restore real page navigation
Goal:
- make previous/next/page input/thumbnail click truly change the visible page in `DxPdfViewer`
Tasks:
- identify supported page navigation mechanism for this DevExpress version
- connect `_currentPage` to the real viewer page
- if direct binding is unavailable, create a supported indirect mechanism
Acceptance for this step:
- previous page works
- next page works
- direct page input works
- `_currentPage` reflects visible page
This step must be completed before signature navigation or page-specific overlays are considered reliable.
#### Step 4 — Stabilize active page geometry extraction
Goal:
- determine a reliable way to identify the active visible rendered page inside the DevExpress DOM
Tasks:
- inspect the actual DOM produced by `DxPdfViewer`
- identify stable selectors for:
- viewer scroll container
- active page surface
- page content bounds
- replace heuristic “largest visible element” logic if a more stable pattern exists
Acceptance for this step:
- geometry retrieval returns correct page bounds repeatedly
- geometry remains stable after zoom
- geometry remains stable after page changes
#### Step 5 — Restore signature placeholder overlay rendering
Goal:
- make unsigned signature placeholders render at correct positions on the current page
Tasks:
- connect placeholder rendering to verified page bounds
- preserve current filtering rules:
- active page only
- hide already-signed fields
- validate size scaling against visible zoom
Acceptance for this step:
- placeholder appears on correct page
- placeholder appears in correct location
- placeholder size follows zoom
#### Step 6 — Restore applied signature overlays
Goal:
- clicking a placeholder should place the actual signature overlay correctly
Tasks:
- keep current visual block format
- bind position/size to the same verified geometry system as placeholders
- ensure page changes hide/show correctly
Acceptance for this step:
- clicking placeholder places signature correctly
- applied signature stays aligned after zoom
- applied signature stays aligned after page change
#### Step 7 — Restore signature navigation
Goal:
- previous/next signature should work across all pages again
Tasks:
- keep the existing global signature ordering logic
- connect it to real page switching
- connect it to real overlay availability
- connect it to real scroll-to-target behavior
Acceptance for this step:
- next signature works on same page
- next signature jumps across pages
- previous signature works
- current index / signed / unsigned counters are correct
#### Step 8 — Validate and finish thumbnail behavior
Goal:
- make sidebar behavior trustworthy again
Tasks:
- confirm thumbnails still render correctly
- ensure thumbnail click changes actual visible page
- ensure active thumbnail reflects actual page
- keep width persistence and resize behavior
Acceptance for this step:
- thumbnail click works
- active highlight is correct
- width persistence still works
### 6. Recommended order of implementation
The recommended execution order is:
1. verify `DxPdfViewer` real API surface
2. restore zoom
3. restore page navigation
4. stabilize page geometry extraction
5. restore placeholder overlays
6. restore applied signatures
7. restore signature navigation
8. finish thumbnail synchronization
This order is important because later features depend on earlier ones.
### 7. What should not be changed yet
Until the viewer bridge is stable, avoid refactoring these parts unnecessarily:
- receiver authorization flow
- page data loading service
- cached signature flow
- popup tabs and signature capture JS
- signature metadata validation
- signature lock behavior
- restart signing behavior
These parts are not the current problem.
### 8. Next implementation checkpoint
The next actual coding step should be:
- **Step 1 + Step 2** together if possible:
- confirm supported `DxPdfViewer` interaction surface
- restore real zoom behavior first
That is the safest first vertical slice because:
- it is easy to verify visually
- it reduces uncertainty in overlay scaling
- it does not yet require full signature flow completion
### 9. Verified DevExpress API findings and current progress
The following points are now verified for the currently installed `DevExpress.Blazor.PdfViewer` package version `25.2.3`:
**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
The first functional recovery step is zoom restoration.
Implemented direction:
- bind receiver zoom state to `DxPdfViewer.ZoomLevel`
- remove competing custom JS ctrl+wheel zoom logic
- keep overlay redraw pipeline in place
- avoid `ZoomLevelChanged` usage because it is not available on the installed `25.2.3` `DxPdfViewer` surface
Expected outcome after this step:
- built-in DevExpress zoom UI can be used without the runtime parameter exception
- custom zoom duplication is removed
### 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. FINAL DECISION: CustomizeToolbar + Manual State Tracking
**Implementation strategy (verified for v25.2.3):**
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()`
2. **Preserve signature-specific toolbar** (separate from DxPdfViewer toolbar)
- Signature change button
- Previous/Next signature navigation
- Signature counter (signed/unsigned/total)
- Reset button
3. **Correct DxPdfViewer usage:**
```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
**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
- 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
**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
}
```
**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
---
## Files Most Likely Relevant For Future Work
### Main page
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
### JavaScript
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
### CSS
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
### Data/auth services
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
---
## Summary
The active receiver document experience is currently a `PDF.js`-based, server-orchestrated, overlay-signing workflow in `EnvelopeGenerator.Server`.
It supports:
- single-page PDF display
- page navigation
- zoom
- thumbnails
- receiver-specific signature placeholders
- actual signature overlay placement
- fast signature navigation across pages
- overlay rescaling/repositioning during zoom
- signature capture via draw/text/image
- cached signature reuse
The current migration goal is to move this experience to `DxPdfViewer` in `EnvelopeReceiverPage.razor` without losing any of the capabilities listed above.
Any future changes in this area should be made with the assumption that this is a custom receiver signing experience whose rendering engine may change, but whose functional behavior must remain intact; it is not a basic document embed and not a direct PDF stamping pipeline.