Added `ZoomLevelChanged` to available events in `DEVEXPRESS_V25_LIMITATIONS.md` and clarified missing events. Created a detailed migration plan in `MIGRATION_PLAN.md` for transitioning from PDF.js to DevExpress DxPdfViewer v25.2.3, including a hybrid approach, testing checklist, and rollback plan. Summarized research findings and API verification in `SESSION_SUMMARY.md`, highlighting limitations, risks, and recommendations for the migration.
12 KiB
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 bindableZoomLevel(double) - Two-way bindable, factor not percentage (1.5 = 150%)ActivePageIndex(int) - Read-only, 0-basedPageCount(int) - Read-onlyIsSinglePagePreview(bool)CssClass(string)DocumentName(string)SizeMode(SizeMode?)
Available Events
CustomizeToolbar- Toolbar customizationZoomLevelChanged- EventCallback when zoom changes
Available Methods
PrintAsync()- Browser print dialogDownloadAsync()- Download PDF
NOT Available (Critical Gaps)
- ❌ No
GoToPageAsync()or any programmatic page navigation - ❌ No
ActivePageIndexChangedorPageNumberChangedevent - ❌
ActivePageIndexis 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:
- DevExpress DxPdfViewer for PDF rendering
- Custom toolbar via
CustomizeToolbarevent - Manual state tracking for current page/zoom
- ZoomLevelChanged event for zoom synchronization
- JavaScript overlays for signature buttons (same as PDF.js implementation)
- 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:
<canvas id="pdfCanvas" style="@CanvasStyle"></canvas>
New:
<DxPdfViewer @ref="_pdfViewer"
DocumentContent="@_pdfDocumentContent"
@bind-ZoomLevel="_viewerZoomLevel"
ZoomLevelChanged="OnZoomLevelChanged"
CustomizeToolbar="OnCustomizeToolbar"
IsSinglePagePreview="true"
CssClass="receiver-pdf-viewer" />
Step 2: Add Component Fields
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
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
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
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
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)
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)
[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)
[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
.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:
- Keep PDF.js files in
wwwroot/js/ - Create branch
feature/devexpress-migrationbefore starting - Master branch keeps PDF.js implementation
- Can revert by checking out master
Success Criteria
Migration is successful if:
- ✅ PDF renders correctly in DevExpress viewer
- ✅ Custom toolbar navigation works
- ✅ Zoom controls work and synchronize
- ✅ Signature overlays render correctly
- ✅ Signature capture and application works
- ✅ Performance is acceptable (no lag on 20+ page PDFs)
- ✅ Mobile/tablet layout works
- ⚠️ 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
- ✅ Verify DevExpress API capabilities (DONE - this document)
- ⬜ Create feature branch
feature/devexpress-migration - ⬜ Backup current EnvelopeReceiverPage.razor
- ⬜ Implement Steps 1-10
- ⬜ Complete testing checklist
- ⬜ Manual testing with real envelopes
- ⬜ Document known limitations for users
- ⬜ Merge to master or rollback based on results