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:
@@ -185,11 +185,79 @@ For signature placeholders, the service:
|
|||||||
Current receiver viewer characteristics:
|
Current receiver viewer characteristics:
|
||||||
- route: `/envelope/{EnvelopeKey}`
|
- route: `/envelope/{EnvelopeKey}`
|
||||||
- render mode: `InteractiveServer`
|
- render mode: `InteractiveServer`
|
||||||
- PDF rendering: `PDF.js`
|
- PDF rendering: **Migration in progress from `PDF.js` to `DxPdfViewer`**
|
||||||
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
- toolbar: page navigation, zoom, thumbnail toggle, signature navigation, signature reset
|
||||||
- signature popup: `DxPopup`
|
- signature popup: `DxPopup`
|
||||||
- thumbnail sidebar: resizable and stored in `localStorage`
|
- thumbnail sidebar: resizable and stored in `localStorage`
|
||||||
|
|
||||||
|
### ⚠️ CRITICAL: DevExpress DxPdfViewer Control Requirements
|
||||||
|
|
||||||
|
**Verified API for installed `DevExpress.Blazor.PdfViewer` v25.2.3:**
|
||||||
|
|
||||||
|
| Property | Access | Notes |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF as `byte[]` |
|
||||||
|
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor** (not percentage): `1.5` = 150% |
|
||||||
|
| `IsSinglePagePreview` | `[Parameter]` GET/SET | Single page mode |
|
||||||
|
| `CssClass` | `[Parameter]` GET/SET | CSS class |
|
||||||
|
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
|
||||||
|
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
|
||||||
|
| `PageCount` | Read-only GET | Total pages — **no JS call needed** |
|
||||||
|
| `ActivePageIndex` | Read-only GET | Current page (0-based) — **cannot SET** |
|
||||||
|
| `CustomizeToolbar` | Event | Only available toolbar event |
|
||||||
|
|
||||||
|
**Does NOT exist in v25.2.3 — do NOT use:**
|
||||||
|
- `GoToPageAsync()` ❌
|
||||||
|
- `GoToNextPageAsync()` ❌
|
||||||
|
- `ZoomAsync()` ❌
|
||||||
|
- `PageNumberChanged` event ❌
|
||||||
|
- `ZoomLevelChanged` event ❌
|
||||||
|
- `ToolbarVisible` property ❌
|
||||||
|
|
||||||
|
**Correct approach:**
|
||||||
|
```razor
|
||||||
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
DocumentContent="@_pdfDocumentContent"
|
||||||
|
ZoomLevel="@_viewerZoomLevel"
|
||||||
|
IsSinglePagePreview="true"
|
||||||
|
CustomizeToolbar="OnCustomizeToolbar" />
|
||||||
|
```
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ZoomLevel: always divide by 100 (factor, not percentage)
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
|
||||||
|
// PageCount: read directly, no JS needed
|
||||||
|
_totalPages = _pdfViewer.PageCount;
|
||||||
|
|
||||||
|
// Page navigation: only via CustomizeToolbar buttons
|
||||||
|
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||||
|
{
|
||||||
|
toolbarModel.AllItems.Clear();
|
||||||
|
var nextButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
IconCssClass = "dx-icon-chevronnext",
|
||||||
|
Enabled = _currentPage < _totalPages,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
_currentPage++;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
toolbarModel.AllItems.Add(nextButton);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript role after migration:**
|
||||||
|
- Overlay geometry calculations only
|
||||||
|
- Thumbnail rendering via PDF.js helper
|
||||||
|
- Custom UI interactions (sidebar resize, signature canvas)
|
||||||
|
- **NOT** for controlling DxPdfViewer page or zoom
|
||||||
|
|
||||||
|
See `DEVEXPRESS_V25_LIMITATIONS.md` for complete verified API reference.
|
||||||
|
|
||||||
### JS Assets
|
### JS Assets
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
|
|||||||
300
DEBUG_NOTES.md
Normal file
300
DEBUG_NOTES.md
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
# Debug Tools for DevExpress DxPdfViewer Integration
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This document describes temporary debug tools added to diagnose DevExpress DOM structure and page navigation issues.
|
||||||
|
|
||||||
|
## IMPORTANT: TEMPORARY DEBUG CODE
|
||||||
|
|
||||||
|
**These debug tools are TEMPORARY and should be REMOVED after resolving the page navigation issue.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Debug Tools Added
|
||||||
|
|
||||||
|
### 1. Debug UI Button (Toolbar)
|
||||||
|
|
||||||
|
**Location:** `EnvelopeReceiverPage.razor` - Toolbar Section
|
||||||
|
|
||||||
|
**Visual:** Orange button with "?" icon in the PDF viewer toolbar
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
- Opens a floating overlay panel showing DevExpress DOM analysis
|
||||||
|
- Displays all input elements found in DxPdfViewer
|
||||||
|
- Shows which CSS selectors successfully find the page input
|
||||||
|
- Provides a "Test: Go to Page 2" button for live testing
|
||||||
|
|
||||||
|
**Code Location:**
|
||||||
|
```razor
|
||||||
|
@* DEBUG: DevExpress DOM Inspector *@
|
||||||
|
<div class="pdf-toolbar__section">
|
||||||
|
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
**C# Method:**
|
||||||
|
```csharp
|
||||||
|
async Task ShowDebugUI()
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("dxPdfViewerShowDebugUI");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. JavaScript Debug Functions
|
||||||
|
|
||||||
|
**Location:** `pdf-viewer.js`
|
||||||
|
|
||||||
|
**Functions Added:**
|
||||||
|
|
||||||
|
#### `window.dxPdfViewerDebugDOM()`
|
||||||
|
- Console-based debug function
|
||||||
|
- Logs detailed DOM analysis to browser console
|
||||||
|
- Returns analysis object for programmatic inspection
|
||||||
|
|
||||||
|
#### `window.dxPdfViewerShowDebugUI()`
|
||||||
|
- HTML overlay-based debug function
|
||||||
|
- Creates visual debug panel without console interaction
|
||||||
|
- No security warnings (no need to paste code)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Step 1: Run Application
|
||||||
|
```powershell
|
||||||
|
dotnet run --project EnvelopeGenerator.Server/EnvelopeGenerator.Server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Open Receiver Page
|
||||||
|
Navigate to: `https://localhost:8088/envelope/{EnvelopeKey}`
|
||||||
|
|
||||||
|
### Step 3: Click Debug Button
|
||||||
|
- Look for the **orange "?" button** in the PDF toolbar (left side, after thumbnails toggle)
|
||||||
|
- Click it to open the debug overlay
|
||||||
|
|
||||||
|
### Step 4: Review Debug Information
|
||||||
|
|
||||||
|
The overlay shows:
|
||||||
|
- **Total Inputs**: Number of input elements found
|
||||||
|
- **Input Elements**: Details of each input (type, class, ID, value)
|
||||||
|
- **Selector Tests**: Which CSS selectors work (✓) and which don't (✗)
|
||||||
|
- **Toolbar**: Whether toolbar element was found
|
||||||
|
- **DxWidget**: Whether DevExpress widget element was found
|
||||||
|
|
||||||
|
### Step 5: Test Page Navigation
|
||||||
|
Click the **"Test: Go to Page 2"** button in the overlay
|
||||||
|
|
||||||
|
### Step 6: Report Results
|
||||||
|
|
||||||
|
**Copy the following information:**
|
||||||
|
|
||||||
|
1. **Total Inputs**: X
|
||||||
|
2. **Input Details**: (type, className, id for each input)
|
||||||
|
3. **Selector Test Results**: (which selectors show ✓ FOUND)
|
||||||
|
4. **Test Result**: Did PDF actually navigate to page 2? (Yes/No)
|
||||||
|
5. **Console Messages**: Any errors or warnings in F12 console
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to Look For
|
||||||
|
|
||||||
|
### ✓ Success Indicators
|
||||||
|
- At least one selector shows **✓ FOUND**
|
||||||
|
- "Test: Go to Page 2" button actually changes PDF page
|
||||||
|
- Console shows: `✓ Found page input with selector: "..."`
|
||||||
|
|
||||||
|
### ✗ Problem Indicators
|
||||||
|
- All selectors show **✗ NOT FOUND**
|
||||||
|
- "Test: Go to Page 2" does nothing
|
||||||
|
- Console shows: `✗ Page input not found`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Diagnosis
|
||||||
|
|
||||||
|
Once the correct selector is identified:
|
||||||
|
|
||||||
|
### 1. Update `window.dxPdfViewerGoToPage()`
|
||||||
|
Update the `selectors` array in `pdf-viewer.js` to prioritize the working selector:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const selectors = [
|
||||||
|
'WORKING_SELECTOR_HERE', // ✓ Move this to top
|
||||||
|
'input[type="number"]',
|
||||||
|
// ... rest
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Remove Debug Code
|
||||||
|
|
||||||
|
**Files to clean up:**
|
||||||
|
|
||||||
|
#### `EnvelopeReceiverPage.razor`
|
||||||
|
Remove:
|
||||||
|
```razor
|
||||||
|
@* DEBUG: DevExpress DOM Inspector *@
|
||||||
|
<div class="pdf-toolbar__section">
|
||||||
|
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Remove C# method:
|
||||||
|
```csharp
|
||||||
|
async Task ShowDebugUI() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `pdf-viewer.js`
|
||||||
|
Remove:
|
||||||
|
```javascript
|
||||||
|
// ⚠ AUTO-DEBUG: Display results in HTML overlay
|
||||||
|
window.dxPdfViewerShowDebugUI = function() { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep:
|
||||||
|
- `window.dxPdfViewerDebugDOM()` - can be useful for future debugging (optional)
|
||||||
|
- `window.dxPdfViewerGoToPage()` - this is permanent (after fixing selector)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Debug UI doesn't open
|
||||||
|
- Check browser console (F12) for JavaScript errors
|
||||||
|
- Ensure `pdf-viewer.js` is loaded
|
||||||
|
- Verify DxPdfViewer has finished rendering
|
||||||
|
|
||||||
|
### "Page input not found" error
|
||||||
|
- DevExpress may not have rendered toolbar yet
|
||||||
|
- Try waiting 2-3 seconds after page load
|
||||||
|
- Check if DxPdfViewer is visible on screen
|
||||||
|
|
||||||
|
### Selector works but page doesn't change
|
||||||
|
- DevExpress may require different event sequence
|
||||||
|
- Try adding more events (focus, click, etc.)
|
||||||
|
- May need to find DevExpress client API instead
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SOLUTION: CustomizeToolbar + Manual State Tracking
|
||||||
|
|
||||||
|
**Identified root cause:**
|
||||||
|
- DevExpress v25.2.3 has no event support
|
||||||
|
- `PageNumberChanged` event does not exist
|
||||||
|
- `ZoomLevelChanged` event does not exist
|
||||||
|
- `ToolbarVisible` property does not exist
|
||||||
|
- `GoToPageAsync()` method does not exist
|
||||||
|
- Only `CustomizeToolbar` event is available
|
||||||
|
|
||||||
|
**Verified working API (v25.2.3):**
|
||||||
|
- `DocumentContent` byte[] – for feeding PDF ✓
|
||||||
|
- `ZoomLevel` double – zoom factor (1.5 = 150%) ✓
|
||||||
|
- `IsSinglePagePreview` bool – single page mode ✓
|
||||||
|
- `PageCount` int (GET only) – **replaces JS call** ✓
|
||||||
|
- `ActivePageIndex` int (GET only) – current page index ✓
|
||||||
|
- `CssClass`, `DocumentName`, `SizeMode` ✓
|
||||||
|
|
||||||
|
**Implemented strategy:**
|
||||||
|
- Create custom navigation/zoom buttons via `CustomizeToolbar`
|
||||||
|
- Manual state tracking with `_currentPage`, `_currentZoom`, `_viewerZoomLevel`
|
||||||
|
- Manually trigger overlay refresh after button clicks
|
||||||
|
- Replace JS getTotalPages() call with `_totalPages = _pdfViewer.PageCount`
|
||||||
|
|
||||||
|
**Correct code example:**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||||
|
{
|
||||||
|
toolbarModel.AllItems.Clear();
|
||||||
|
|
||||||
|
var prevButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
Text = "Previous",
|
||||||
|
IconCssClass = "dx-icon-chevronprev",
|
||||||
|
Enabled = _currentPage > 1,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
if (_currentPage > 1)
|
||||||
|
{
|
||||||
|
_currentPage--;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var nextButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
Text = "Next",
|
||||||
|
IconCssClass = "dx-icon-chevronnext",
|
||||||
|
Enabled = _currentPage < _totalPages,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
if (_currentPage < _totalPages)
|
||||||
|
{
|
||||||
|
_currentPage++;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toolbarModel.AllItems.Add(prevButton);
|
||||||
|
toolbarModel.AllItems.Add(nextButton);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**PageCount usage (instead of JS):**
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// In OnAfterRenderAsync
|
||||||
|
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||||
|
{
|
||||||
|
_totalPages = _pdfViewer.PageCount; // JS getTotalPages() no longer needed
|
||||||
|
_pdfLoaded = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Known limitations:**
|
||||||
|
1. If user scrolls PDF, C# receives no notification, overlays may desync
|
||||||
|
2. Thumbnail navigation only updates state, cannot move viewer
|
||||||
|
3. Cross-page signature navigation limited without programmatic page switching
|
||||||
|
|
||||||
|
**See:** `DEVEXPRESS_V25_LIMITATIONS.md` – complete verified API reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Expected Timeline
|
||||||
|
|
||||||
|
1. ✓ **Day 1**: Add debug tools (DONE)
|
||||||
|
2. ✓ **Day 1**: Collect DOM analysis data (DONE)
|
||||||
|
3. ✓ **Day 1**: Identify root cause (DONE - v25.2.3 has no events)
|
||||||
|
4. ✓ **Day 1**: Define workaround strategy (DONE - Custom toolbar with manual tracking)
|
||||||
|
5. ✓ **Day 1**: Implement workaround (DONE)
|
||||||
|
6. ⚠ **Day 2**: Test and document limitations
|
||||||
|
7. ⚠ **Day 2**: Consider DevExpress upgrade or accept limitations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Files
|
||||||
|
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
|
- `RECEIVER_PDF_VIEWER_CONTEXT.md` (main context document - **UPDATED with new strategy**)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Debug UI uses inline styles to avoid CSS conflicts
|
||||||
|
- Overlay is positioned at `z-index: 99999` to appear above everything
|
||||||
|
- Close button removes overlay from DOM completely
|
||||||
|
- All debug output also goes to browser console for advanced inspection
|
||||||
|
- **Debug findings led to complete strategy change - see RECEIVER_PDF_VIEWER_CONTEXT.md section 12-14**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Remember: This is TEMPORARY debugging code. Delete after completing the new implementation strategy!**
|
||||||
233
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
233
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# DevExpress Blazor PdfViewer v25.2.3 - Verified API Reference
|
||||||
|
|
||||||
|
> **Source:** All information in this document has been verified from the actual source code of `DevExpress.Blazor.PdfViewer` v25.2.3 package.
|
||||||
|
> AI-generated API suggestions (GoToPageAsync, PageNumberChanged, etc.) are NOT real – do not use them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verified Available Parameters
|
||||||
|
|
||||||
|
| Property | Type | Access | Default | Description |
|
||||||
|
|----------|------|--------|---------|-------------|
|
||||||
|
| `DocumentContent` | `byte[]` | `[Parameter]` GET/SET | – | Feeds PDF content as byte array |
|
||||||
|
| `CssClass` | `string` | `[Parameter]` GET/SET | – | Assigns CSS class to component |
|
||||||
|
| `DocumentName` | `string` | `[Parameter]` GET/SET | `"Document"` | Download filename |
|
||||||
|
| `IsSinglePagePreview` | `bool` | `[Parameter]` GET/SET | `false` | Single page mode |
|
||||||
|
| `SizeMode` | `SizeMode?` | `[Parameter]` GET/SET | `null` | `Small`, `Medium`, `Large` |
|
||||||
|
| `ZoomLevel` | `double` | `[Parameter]` GET/SET | `-1` | **Factor** (not percentage). `1.5` = 150% |
|
||||||
|
| `ActivePageIndex` | `int` | GET only | – | Active page index (0-based). No SET. |
|
||||||
|
| `PageCount` | `int` | GET only | – | Total page count in document |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Missing Events (NOT AVAILABLE in v25.2.3)
|
||||||
|
|
||||||
|
- **`PageNumberChanged`** – Not available
|
||||||
|
- **`ZoomLevelChanged`** – Not available
|
||||||
|
- User scrolling or native toolbar page changes do not trigger C# code
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Missing Properties (NOT AVAILABLE in v25.2.3)
|
||||||
|
|
||||||
|
- **`ToolbarVisible`** – Not available (toolbar cannot be completely hidden)
|
||||||
|
- **`ActivePageIndex` (settable)** – Read-only; no programmatic page navigation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Missing Methods (NOT AVAILABLE in v25.2.3)
|
||||||
|
|
||||||
|
- **`GoToPageAsync()`** – Not available
|
||||||
|
- **`GoToNextPageAsync()`** – Not available
|
||||||
|
- **`ZoomAsync()`** – Not available
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Event
|
||||||
|
|
||||||
|
- **`CustomizeToolbar`** – Only available event for toolbar customization
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Integration Notes
|
||||||
|
|
||||||
|
### ZoomLevel takes factor, not percentage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// CORRECT
|
||||||
|
_viewerZoomLevel = 1.5; // viewer displays "150%"
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // _currentZoom=150 -> 1.5
|
||||||
|
|
||||||
|
// WRONG
|
||||||
|
_viewerZoomLevel = 150; // viewer displays "15000%"
|
||||||
|
```
|
||||||
|
|
||||||
|
### PageCount replaces JS call
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// CORRECT - read directly from component (no JS needed)
|
||||||
|
_totalPages = _pdfViewer.PageCount;
|
||||||
|
|
||||||
|
// OLD method (no longer needed for this purpose)
|
||||||
|
// _totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
||||||
|
```
|
||||||
|
|
||||||
|
### ActivePageIndex is read-only
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// CORRECT - read for state synchronization
|
||||||
|
var currentPage = _pdfViewer.ActivePageIndex + 1; // convert to 1-based
|
||||||
|
|
||||||
|
// COMPILE ERROR - no setter
|
||||||
|
// _pdfViewer.ActivePageIndex = 3; // COMPILE ERROR
|
||||||
|
```
|
||||||
|
|
||||||
|
### DocumentContent byte[] feeding
|
||||||
|
|
||||||
|
```razor
|
||||||
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
DocumentContent="@_pdfDocumentContent"
|
||||||
|
ZoomLevel="@_viewerZoomLevel"
|
||||||
|
IsSinglePagePreview="true" />
|
||||||
|
|
||||||
|
@code {
|
||||||
|
DxPdfViewer? _pdfViewer;
|
||||||
|
byte[]? _pdfDocumentContent; // populate in OnInitializedAsync
|
||||||
|
double _viewerZoomLevel = 1.5; // 150%
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Impact on EnvelopeReceiverPage
|
||||||
|
|
||||||
|
### Features That Don't Work
|
||||||
|
1. **Event-driven overlay updates** – No page/zoom change events
|
||||||
|
2. **Thumbnail click navigation** – Cannot navigate viewer to specific page via C# API
|
||||||
|
3. **Cross-page signature navigation** – No programmatic page change API
|
||||||
|
4. **Automatic overlay synchronization** – User scroll/native toolbar doesn't trigger C#
|
||||||
|
|
||||||
|
### Features That Work
|
||||||
|
1. **ZoomLevel binding** – Custom zoom buttons can update viewer zoom
|
||||||
|
2. **PageCount** – Total pages can be read directly from component
|
||||||
|
3. **IsSinglePagePreview** – Single page mode works
|
||||||
|
4. **DocumentContent** – byte[] feeding works perfectly
|
||||||
|
5. **CustomizeToolbar** – Only way to add custom buttons to toolbar
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workaround Strategy
|
||||||
|
|
||||||
|
CustomizeToolbar event is used to add custom navigation/zoom buttons.
|
||||||
|
Manual state tracking (`_currentPage`, `_currentZoom`, `_viewerZoomLevel`) is kept in C#.
|
||||||
|
Overlay refresh is manually triggered only after button clicks.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||||
|
{
|
||||||
|
toolbarModel.AllItems.Clear();
|
||||||
|
|
||||||
|
var prevButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
Text = "Previous",
|
||||||
|
IconCssClass = "dx-icon-chevronprev",
|
||||||
|
Enabled = _currentPage > 1,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
if (_currentPage > 1)
|
||||||
|
{
|
||||||
|
_currentPage--;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var nextButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
Text = "Next",
|
||||||
|
IconCssClass = "dx-icon-chevronnext",
|
||||||
|
Enabled = _currentPage < _totalPages,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
if (_currentPage < _totalPages)
|
||||||
|
{
|
||||||
|
_currentPage++;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var zoomInButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
IconCssClass = "dx-icon-plus",
|
||||||
|
Enabled = _currentZoom < 300,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
_currentZoom = Math.Min(_currentZoom + 10, 300);
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var zoomOutButton = new ToolbarItem
|
||||||
|
{
|
||||||
|
IconCssClass = "dx-icon-minus",
|
||||||
|
Enabled = _currentZoom > 50,
|
||||||
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
_currentZoom = Math.Max(_currentZoom - 10, 50);
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
toolbarModel.AllItems.Add(prevButton);
|
||||||
|
toolbarModel.AllItems.Add(nextButton);
|
||||||
|
toolbarModel.AllItems.Add(zoomInButton);
|
||||||
|
toolbarModel.AllItems.Add(zoomOutButton);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PageCount reading example (in OnAfterRenderAsync)
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 })
|
||||||
|
{
|
||||||
|
await Task.Delay(300); // wait for DxPdfViewer to load
|
||||||
|
|
||||||
|
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||||
|
{
|
||||||
|
_totalPages = _pdfViewer.PageCount; // read directly instead of JS
|
||||||
|
_pdfLoaded = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderThumbnailsAsync();
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Known Acceptable Limitations
|
||||||
|
|
||||||
|
1. If user scrolls PDF, C# `_currentPage` does not synchronize
|
||||||
|
2. Thumbnail clicks update state but cannot move DevExpress viewer to target page
|
||||||
|
3. Browser zoom gestures do not trigger overlay updates
|
||||||
|
4. Custom toolbar buttons correctly trigger overlay updates
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- DevExpress official documentation: https://docs.devexpress.com/Blazor/DevExpress.Blazor.PdfViewer.DxPdfViewer
|
||||||
|
- Verified package: `DevExpress.Blazor.PdfViewer` v25.2.3
|
||||||
|
- **Note:** AI-suggested APIs (GoToPageAsync, PageNumberChanged, ZoomLevelChanged, ZoomAsync, ToolbarVisible) are NOT real. Do not use.
|
||||||
@@ -64,13 +64,96 @@ This means `DxPdfViewer` must become the main document rendering surface without
|
|||||||
- signature navigation across pages
|
- signature navigation across pages
|
||||||
- overlay repositioning and resizing after zoom/page changes
|
- 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
|
### 3. Custom enhancement layer
|
||||||
Extra behavior is implemented through custom JavaScript and CSS:
|
Extra behavior is implemented through custom JavaScript and CSS:
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
- `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`:
|
The following points are now verified for the currently installed `DevExpress.Blazor.PdfViewer` package version `25.2.3`:
|
||||||
|
|
||||||
- `ZoomLevel` is a real component parameter
|
**Available `[Parameter]` properties (GET/SET):**
|
||||||
- normal positive zoom values used in this receiver page must be treated as factors in the live UI flow here (`1.5` = `150%`)
|
- `DocumentContent` (`byte[]`) — feed PDF as byte array ?
|
||||||
- `ActivePageIndex` is read-only information and is not a page-navigation parameter
|
- `ZoomLevel` (`double`) — zoom factor, not percentage. `1.5` = `150%` ?
|
||||||
- `ZoomLevelChanged` must not be used in this workspace because the current installed component surface does not expose a matching incoming parameter on `DxPdfViewer`
|
- `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
|
### 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`
|
- `_currentZoom` = receiver custom workflow percentage view, for example `150`
|
||||||
- `_viewerZoomLevel` = DevExpress viewer value, for example `1.5`
|
- `_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
|
2. **Preserve signature-specific toolbar** (separate from DxPdfViewer toolbar)
|
||||||
- rely on the built-in DevExpress PDF Viewer zoom UI instead
|
- Signature change button
|
||||||
- keep `ZoomLevelChanged` synchronization so overlay redraw logic can still react to viewer zoom changes
|
- Previous/Next signature navigation
|
||||||
|
- Signature counter (signed/unsigned/total)
|
||||||
|
- Reset button
|
||||||
|
|
||||||
Reason:
|
3. **Correct DxPdfViewer usage:**
|
||||||
|
|
||||||
- DevExpress already provides zoom UX
|
```razor
|
||||||
- duplicate zoom controls create confusing UX and unit mismatch risks
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
- the built-in viewer zoom UI is a better source of truth for the current zoom state
|
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
|
### 13. Current UI decision for thumbnails
|
||||||
|
|
||||||
The custom thumbnail sidebar is still kept for now.
|
**Decision: Keep custom thumbnail sidebar**
|
||||||
|
|
||||||
Reason:
|
Reason:
|
||||||
|
|
||||||
- current receiver workflow depends on custom thumbnail shell behavior
|
- Current receiver workflow depends on custom thumbnail shell behavior
|
||||||
- width persistence and resizable splitter are already implemented in the custom sidebar
|
- 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
|
- 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
|
**Future consideration:**
|
||||||
- revisit possible DevExpress-native thumbnail navigation later only if it supports the required receiver workflow behavior
|
- 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
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
161
TESTING_CHECKLIST.md
Normal file
161
TESTING_CHECKLIST.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# DevExpress v25.2.3 - Testing Checklist
|
||||||
|
|
||||||
|
> **Important:** This checklist has been updated according to the verified real API for v25.2.3.
|
||||||
|
> `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible` do NOT exist.
|
||||||
|
|
||||||
|
## Build Status
|
||||||
|
- **Build:** Successful
|
||||||
|
- **DevExpress Version:** 25.2.3
|
||||||
|
- **Strategy:** `CustomizeToolbar` + manual state tracking
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Test Scenarios
|
||||||
|
|
||||||
|
### 1. PDF Loading
|
||||||
|
- [ ] PDF document loads successfully
|
||||||
|
- [ ] DevExpress PdfViewer displays the document
|
||||||
|
- [ ] `_pdfViewer.PageCount > 0` check passes
|
||||||
|
- [ ] `_totalPages = _pdfViewer.PageCount` gets correct value (no JS call needed)
|
||||||
|
- [ ] `_pdfLoaded = true` is set
|
||||||
|
- [ ] Toolbar is visible and shows correct page count (e.g., "Page 1 / 5")
|
||||||
|
- [ ] Zoom level displays correctly (e.g., "150%") – if `_viewerZoomLevel = 1.5`
|
||||||
|
|
||||||
|
### 2. CustomizeToolbar Navigation
|
||||||
|
- [ ] **Previous Page button** (◀) works and updates page counter
|
||||||
|
- [ ] **Next Page button** (▶) works and updates page counter
|
||||||
|
- [ ] Previous button is **disabled on page 1**
|
||||||
|
- [ ] Next button is **disabled on last page**
|
||||||
|
- [ ] Page counter updates correctly (e.g., "Page 2 / 5")
|
||||||
|
- [ ] Each navigation button's Click handler calls `RenderSignatureButtonsAsync()`
|
||||||
|
|
||||||
|
### 3. CustomizeToolbar Zoom
|
||||||
|
- [ ] **Zoom In button** (+) increases zoom
|
||||||
|
- [ ] **Zoom Out button** (−) decreases zoom
|
||||||
|
- [ ] `_viewerZoomLevel = _currentZoom / 100d` is calculated (150 → 1.5)
|
||||||
|
- [ ] Viewer does NOT display **"15000%"** (incorrect value detection)
|
||||||
|
- [ ] Zoom is constrained to 50% - 300% range
|
||||||
|
- [ ] PDF viewer zoom level changes visually
|
||||||
|
|
||||||
|
### 4. Signature Overlay Rendering
|
||||||
|
- [ ] **Signature placeholders** appear on correct pages
|
||||||
|
- [ ] Overlays are positioned correctly over the PDF
|
||||||
|
- [ ] Overlays **re-render after page changes**
|
||||||
|
- [ ] Overlays **re-render after zoom changes**
|
||||||
|
- [ ] Overlay sizes scale with zoom level
|
||||||
|
|
||||||
|
### 5. Signature Navigation
|
||||||
|
- [ ] Custom signature toolbar is visible (if signatures exist)
|
||||||
|
- [ ] Previous/Next signature buttons work
|
||||||
|
- [ ] Signature counter shows correct values (e.g., "0 / 3")
|
||||||
|
- [ ] "X open" badge shows unsigned count
|
||||||
|
- [ ] **Cross-page navigation:** `_currentPage` updates and overlays refresh
|
||||||
|
- [ ] ⚠ **Known limitation:** DxPdfViewer visible page cannot be changed programmatically
|
||||||
|
|
||||||
|
### 6. Thumbnail Sidebar
|
||||||
|
- [ ] Thumbnails render (may take a few seconds)
|
||||||
|
- [ ] Thumbnail click **updates `_currentPage` state**
|
||||||
|
- [ ] Thumbnail click **refreshes overlays**
|
||||||
|
- [ ] ⚠ **Known limitation:** Thumbnail click does not navigate DevExpress viewer
|
||||||
|
- [ ] Active thumbnail is highlighted correctly
|
||||||
|
|
||||||
|
### 7. Signature Capture & Application
|
||||||
|
- [ ] Signature popup opens on first load (if no cache)
|
||||||
|
- [ ] Draw signature works
|
||||||
|
- [ ] Text signature works
|
||||||
|
- [ ] Image upload signature works
|
||||||
|
- [ ] Clicking signature placeholder applies signature
|
||||||
|
- [ ] Applied signature overlays are positioned correctly
|
||||||
|
- [ ] Counter updates after signature applied (e.g., "1 / 3")
|
||||||
|
|
||||||
|
### 8. Known Limitations (v25.2.3 API Limit)
|
||||||
|
- [ ] ⚠ **User cannot scroll PDF to change pages** (only CustomizeToolbar buttons)
|
||||||
|
- [ ] ⚠ Thumbnail clicks do not navigate viewer (only update state)
|
||||||
|
- [ ] ⚠ Browser zoom gestures do not trigger overlay updates
|
||||||
|
- [ ] ✓ Custom toolbar buttons correctly trigger overlay updates
|
||||||
|
- [ ] ✓ `_pdfViewer.PageCount` eliminates need for JS `getTotalPages()` call
|
||||||
|
- [ ] ✓ `ZoomLevel = _currentZoom / 100d` calculates correct zoom factor
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Issue: Toolbar not visible
|
||||||
|
**Reason:** `_pdfLoaded = false` or `_totalPages = 0`
|
||||||
|
**Solution:** In `OnAfterRenderAsync`, check `_pdfViewer.PageCount > 0`, set `_pdfLoaded = true`
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
if (_pdfViewer is not null && _pdfViewer.PageCount > 0)
|
||||||
|
{
|
||||||
|
_totalPages = _pdfViewer.PageCount; // no JS call needed
|
||||||
|
_pdfLoaded = true;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Signature overlays not visible
|
||||||
|
**Reason:** `RenderSignatureButtonsAsync()` not called after page/zoom change
|
||||||
|
**Solution:** Verify each button Click handler in `OnCustomizeToolbar` calls `RenderSignatureButtonsAsync()`
|
||||||
|
|
||||||
|
### Issue: Page navigation not working
|
||||||
|
**Reason:** `OnCustomizeToolbar` button Click lambdas not updating `_currentPage`
|
||||||
|
**Solution:** Check that each button updates `_currentPage` and calls `StateHasChanged()` and `RenderSignatureButtonsAsync()`
|
||||||
|
|
||||||
|
### Issue: Zoom not working or showing "15000%"
|
||||||
|
**Reason:** Incorrect value assigned to `_viewerZoomLevel`
|
||||||
|
**Solution:** `_viewerZoomLevel = _currentZoom / 100d` – ZoomLevel takes **factor**, not percentage
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// CORRECT
|
||||||
|
_currentZoom = 150; // UI display: "150%"
|
||||||
|
_viewerZoomLevel = 150 / 100d; // Pass to DxPdfViewer: 1.5
|
||||||
|
|
||||||
|
// WRONG
|
||||||
|
_viewerZoomLevel = 150; // DxPdfViewer displays "15000%"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue: Total page count is 0
|
||||||
|
**Reason:** JS call made before timing or fails
|
||||||
|
**Solution:** Use `_pdfViewer.PageCount` directly, no need for `pdfViewer.getTotalPages()` JS call
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
**Minimum working functionality:**
|
||||||
|
- PDF loads and displays
|
||||||
|
- `_totalPages = _pdfViewer.PageCount` gets correct page count
|
||||||
|
- Custom toolbar navigation works (previous/next/zoom)
|
||||||
|
- Signature overlays render on current page
|
||||||
|
- Signature capture and application works
|
||||||
|
- Overlays update after navigation/zoom
|
||||||
|
|
||||||
|
**Known acceptable limitations (v25.2.3 API limit):**
|
||||||
|
- Thumbnail clicks do not navigate viewer (only update state)
|
||||||
|
- User scroll/native toolbar navigation does not update C# state
|
||||||
|
- Cross-page signature navigation is limited
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps If All Tests Pass
|
||||||
|
|
||||||
|
1. Remove debug toolbar button (if present)
|
||||||
|
2. Clean up unused code comments
|
||||||
|
3. Update user documentation about navigation limitations
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Architecture Reference: Verified v25.2.3 API
|
||||||
|
|
||||||
|
| Property | Access | Usage |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| `DocumentContent` | `[Parameter]` GET/SET | Feed PDF with `byte[]` |
|
||||||
|
| `ZoomLevel` | `[Parameter]` GET/SET | **Factor**: `_currentZoom / 100d` |
|
||||||
|
| `IsSinglePagePreview` | `[Parameter]` GET/SET | `true` = single page mode |
|
||||||
|
| `PageCount` | GET only | Total pages – no JS needed |
|
||||||
|
| `ActivePageIndex` | GET only | Active page index (0-based) – no SET |
|
||||||
|
| `CssClass` | `[Parameter]` GET/SET | Assign CSS class |
|
||||||
|
| `SizeMode` | `[Parameter]` GET/SET | `Small` / `Medium` / `Large` |
|
||||||
|
| `DocumentName` | `[Parameter]` GET/SET | Download filename |
|
||||||
|
|
||||||
|
**Not available:** `GoToPageAsync()`, `PageNumberChanged`, `ZoomLevelChanged`, `ToolbarVisible`
|
||||||
Reference in New Issue
Block a user