Update DevExpress migration docs and plan
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.
This commit is contained in:
454
MIGRATION_PLAN.md
Normal file
454
MIGRATION_PLAN.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# 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<double> 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
|
||||
<canvas id="pdfCanvas" style="@CanvasStyle"></canvas>
|
||||
```
|
||||
|
||||
**New:**
|
||||
```razor
|
||||
<DxPdfViewer @ref="_pdfViewer"
|
||||
DocumentContent="@_pdfDocumentContent"
|
||||
@bind-ZoomLevel="_viewerZoomLevel"
|
||||
ZoomLevelChanged="OnZoomLevelChanged"
|
||||
CustomizeToolbar="OnCustomizeToolbar"
|
||||
IsSinglePagePreview="true"
|
||||
CssClass="receiver-pdf-viewer" />
|
||||
```
|
||||
|
||||
### 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
|
||||
Reference in New Issue
Block a user