# DevExpress DxPdfViewer Migration Plan ## EnvelopeReceiverPage.razor - PDF.js to DevExpress v25.2.3 **Created:** June 30, 2026 **Target File:** `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` **Current Implementation:** PDF.js 3.11.174 with custom JavaScript overlays **Target Implementation:** DevExpress DxPdfViewer v25.2.3 with hybrid approach --- ## Confirmed DevExpress v25.2.3 API ### Available Properties - `DocumentContent` (byte[]) - Two-way bindable - `ZoomLevel` (double) - Two-way bindable, **factor not percentage** (1.5 = 150%) - `ActivePageIndex` (int) - **Read-only**, 0-based - `PageCount` (int) - **Read-only** - `IsSinglePagePreview` (bool) - `CssClass` (string) - `DocumentName` (string) - `SizeMode` (SizeMode?) ### Available Events - `CustomizeToolbar` - Toolbar customization - `ZoomLevelChanged` - EventCallback when zoom changes ### Available Methods - `PrintAsync()` - Browser print dialog - `DownloadAsync()` - Download PDF ### NOT Available (Critical Gaps) - ❌ No `GoToPageAsync()` or any programmatic page navigation - ❌ No `ActivePageIndexChanged` or `PageNumberChanged` event - ❌ `ActivePageIndex` is read-only (cannot set programmatically) - ❌ No way to detect user scrolling between pages --- ## Migration Strategy: Hybrid Approach Given the API limitations, we'll use a **hybrid approach**: 1. **DevExpress DxPdfViewer** for PDF rendering 2. **Custom toolbar** via `CustomizeToolbar` event 3. **Manual state tracking** for current page/zoom 4. **ZoomLevelChanged event** for zoom synchronization 5. **JavaScript overlays** for signature buttons (same as PDF.js implementation) 6. **Graceful degradation** for features that can't be implemented ### What Works ✅ PDF rendering with DevExpress ✅ Custom zoom controls via toolbar ✅ Zoom level synchronization via `ZoomLevelChanged` event ✅ Signature button overlays (JavaScript, same as current) ✅ Signature capture workflow (unchanged) ✅ Page count display ### What Has Limitations ⚠️ **Page navigation** - Custom toolbar buttons only (no thumbnail click navigation to viewer) ⚠️ **Page tracking** - Manual state only (no event when user scrolls in native viewer) ⚠️ **Thumbnail navigation** - Updates state but cannot move viewer to that page ⚠️ **Signature button clicks** - Cannot navigate viewer to signature page programmatically --- ## Implementation Steps ### Step 1: Update Component Structure **Current:** ```razor ``` **New:** ```razor ``` ### Step 2: Add Component Fields ```csharp private DxPdfViewer? _pdfViewer; private byte[]? _pdfDocumentContent; private double _viewerZoomLevel = 1.0; // DevExpress expects factor (1.0 = 100%) private int _currentPage = 1; // Manual tracking (1-based) private int _currentZoom = 100; // Manual tracking (percentage) private int _totalPages = 0; ``` ### Step 3: Implement CustomizeToolbar ```csharp protected void OnCustomizeToolbar(ToolbarModel toolbarModel) { toolbarModel.AllItems.Clear(); // Previous Page Button toolbarModel.AllItems.Add(new ToolbarItem { Text = "Previous", IconCssClass = "dx-icon-chevronprev", Enabled = _currentPage > 1, Click = async (args) => { if (_currentPage > 1) { _currentPage--; await RefreshOverlaysAsync(); } } }); // Page Info Display toolbarModel.AllItems.Add(new ToolbarItem { Text = $"Page {_currentPage} of {_totalPages}", BeginGroup = true }); // Next Page Button toolbarModel.AllItems.Add(new ToolbarItem { Text = "Next", IconCssClass = "dx-icon-chevronnext", Enabled = _currentPage < _totalPages, Click = async (args) => { if (_currentPage < _totalPages) { _currentPage++; await RefreshOverlaysAsync(); } } }); // Zoom Out Button toolbarModel.AllItems.Add(new ToolbarItem { IconCssClass = "dx-icon-minus", BeginGroup = true, Enabled = _currentZoom > 50, Click = async (args) => { _currentZoom = Math.Max(_currentZoom - 10, 50); _viewerZoomLevel = _currentZoom / 100.0; await InvokeAsync(StateHasChanged); } }); // Zoom Display toolbarModel.AllItems.Add(new ToolbarItem { Text = $"{_currentZoom}%" }); // Zoom In Button toolbarModel.AllItems.Add(new ToolbarItem { IconCssClass = "dx-icon-plus", Enabled = _currentZoom < 300, Click = async (args) => { _currentZoom = Math.Min(_currentZoom + 10, 300); _viewerZoomLevel = _currentZoom / 100.0; await InvokeAsync(StateHasChanged); } }); // Download Button toolbarModel.AllItems.Add(new ToolbarItem { IconCssClass = "dx-icon-download", BeginGroup = true, Click = async (args) => { if (_pdfViewer != null) { await _pdfViewer.DownloadAsync(); } } }); } ``` ### Step 4: Implement ZoomLevelChanged Event ```csharp private async Task OnZoomLevelChanged(double newZoomLevel) { // Synchronize manual tracking with DevExpress viewer _currentZoom = (int)Math.Round(newZoomLevel * 100); // Refresh signature overlays with new zoom await RefreshOverlaysAsync(); // Force toolbar update to show new zoom percentage await InvokeAsync(StateHasChanged); } ``` ### Step 5: Load PDF Document ```csharp protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); if (!string.IsNullOrEmpty(_envelopeKey)) { // Existing envelope loading logic... var envelope = await EnvelopeService.GetEnvelopeByKeyAsync(_envelopeKey); if (envelope?.Document?.BinaryContent != null) { _pdfDocumentContent = envelope.Document.BinaryContent; } } } ``` ### Step 6: Read PageCount After Render ```csharp protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (!_pdfLoaded && _pdfDocumentContent is { Length: > 0 }) { // Wait for DevExpress to load PDF await Task.Delay(300); if (_pdfViewer is not null && _pdfViewer.PageCount > 0) { _totalPages = _pdfViewer.PageCount; _pdfLoaded = true; // Initial overlay render await RefreshOverlaysAsync(); await InvokeAsync(StateHasChanged); } } } ``` ### Step 7: Signature Overlay Rendering (Keep JavaScript) ```csharp private async Task RefreshOverlaysAsync() { if (!_pdfLoaded || _signatures == null) return; // Filter signatures for current page var currentPageSignatures = _signatures .Where(s => s.PageNumber == _currentPage) .ToList(); // Call JavaScript to render overlays (same as PDF.js implementation) await JSRuntime.InvokeVoidAsync( "pdfViewer.renderSignatureButtons", currentPageSignatures, _currentPage, DotNetObjectReference.Create(this) ); } ``` ### Step 8: Handle Thumbnail Clicks (Best Effort) ```csharp [JSInvokable] public async Task OnThumbnailClick(int pageNumber) { if (pageNumber < 1 || pageNumber > _totalPages) return; // Update manual state _currentPage = pageNumber; // Refresh overlays await RefreshOverlaysAsync(); await InvokeAsync(StateHasChanged); // NOTE: DevExpress viewer will NOT navigate to this page // User must use custom toolbar buttons to navigate // This is a known limitation of v25.2.3 } ``` ### Step 9: Handle Signature Button Clicks (Best Effort) ```csharp [JSInvokable] public async Task OnSignatureButtonClick(string signatureId) { var signature = _signatures?.FirstOrDefault(s => s.Id == signatureId); if (signature == null) return; // Update to signature's page _currentPage = signature.PageNumber; // Open signature modal _showSignatureModal = true; _selectedSignatureId = signatureId; await RefreshOverlaysAsync(); await InvokeAsync(StateHasChanged); // NOTE: DevExpress viewer will NOT navigate to signature page // User sees modal but viewer stays on current page // This is a known limitation of v25.2.3 } ``` ### Step 10: Update CSS for DevExpress ```css .receiver-pdf-viewer { width: 100%; height: 600px; border: 1px solid #dee2e6; border-radius: 4px; } /* Signature overlay buttons (same as PDF.js) */ .signature-button { position: absolute; border: 2px solid #0d6efd; background-color: rgba(13, 110, 253, 0.1); cursor: pointer; transition: all 0.2s; } .signature-button:hover { background-color: rgba(13, 110, 253, 0.3); border-color: #0a58ca; } .signature-button.signed { border-color: #198754; background-color: rgba(25, 135, 84, 0.1); } ``` --- ## Testing Checklist ### Phase 1: Basic PDF Rendering - [ ] PDF loads in DevExpress viewer - [ ] PageCount is correctly read - [ ] Initial zoom is 100% - [ ] Custom toolbar appears with all buttons ### Phase 2: Navigation - [ ] Previous button navigates (updates state) - [ ] Next button navigates (updates state) - [ ] Page info displays correctly - [ ] Buttons disable at first/last page ### Phase 3: Zoom - [ ] Zoom in button increases zoom - [ ] Zoom out button decreases zoom - [ ] Zoom display shows correct percentage - [ ] ZoomLevelChanged event fires - [ ] Buttons disable at 50%/300% ### Phase 4: Signature Overlays - [ ] Signature buttons render on correct positions - [ ] Overlays update when page changes (custom toolbar) - [ ] Overlays update when zoom changes - [ ] Click opens signature modal - [ ] Signed signatures show green border ### Phase 5: Signature Workflow - [ ] Draw signature works - [ ] Type signature works - [ ] Upload image works - [ ] Signature applies to PDF - [ ] Overlay updates to "signed" state ### Phase 6: Edge Cases - [ ] Multi-page PDF (10+ pages) - [ ] PDF with no signatures - [ ] PDF with multiple signatures on same page - [ ] Browser refresh preserves state - [ ] Mobile responsive layout ### Known Limitations to Document - [ ] User scrolling in viewer doesn't update custom toolbar page number - [ ] Thumbnail clicks don't navigate viewer (state updates only) - [ ] Signature button clicks don't navigate viewer to that page - [ ] Native DevExpress toolbar is hidden (custom toolbar only) --- ## Rollback Plan If migration fails or critical issues are discovered: 1. Keep PDF.js files in `wwwroot/js/` 2. Create branch `feature/devexpress-migration` before starting 3. Master branch keeps PDF.js implementation 4. Can revert by checking out master --- ## Success Criteria Migration is successful if: 1. ✅ PDF renders correctly in DevExpress viewer 2. ✅ Custom toolbar navigation works 3. ✅ Zoom controls work and synchronize 4. ✅ Signature overlays render correctly 5. ✅ Signature capture and application works 6. ✅ Performance is acceptable (no lag on 20+ page PDFs) 7. ✅ Mobile/tablet layout works 8. ⚠️ User is informed about navigation limitations (documentation/tooltips) --- ## Timeline Estimate - **Step 1-2:** Component structure update - 30 minutes - **Step 3:** CustomizeToolbar implementation - 1 hour - **Step 4:** ZoomLevelChanged event - 30 minutes - **Step 5-6:** PDF loading and PageCount - 30 minutes - **Step 7:** Signature overlays - 1 hour (testing positioning) - **Step 8-9:** Thumbnail/signature navigation - 1 hour - **Step 10:** CSS updates - 30 minutes - **Testing:** Full checklist - 2 hours - **Documentation:** User-facing limitations - 30 minutes **Total Estimated Time:** 7-8 hours --- ## Next Steps 1. ✅ Verify DevExpress API capabilities (DONE - this document) 2. ⬜ Create feature branch `feature/devexpress-migration` 3. ⬜ Backup current EnvelopeReceiverPage.razor 4. ⬜ Implement Steps 1-10 5. ⬜ Complete testing checklist 6. ⬜ Manual testing with real envelopes 7. ⬜ Document known limitations for users 8. ⬜ Merge to master or rollback based on results