Files
EnvelopeGenerator/MIGRATION_PLAN.md
TekH 732fe92952 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.
2026-06-30 18:32:25 +02:00

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 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:

<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:

  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