Migrate PDF.js to DevExpress DxPdfViewer

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

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

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

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

View File

@@ -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
View 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!**

View 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.

View File

@@ -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
View 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`