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