Compare commits
10 Commits
7b912387e7
...
feat/migr-
| Author | SHA1 | Date | |
|---|---|---|---|
| 732fe92952 | |||
| 99fbb33f1c | |||
| a10ee590c9 | |||
| 03367ebc4a | |||
| 1ac7188466 | |||
| db593cb46a | |||
| a5e4f97397 | |||
| 6ca03a50eb | |||
| 96a84ba1a5 | |||
| ec0ea72890 |
@@ -1,564 +1,419 @@
|
|||||||
# EnvelopeGenerator — AI Context Reference
|
# EnvelopeGenerator — Current Workspace Context
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
Digital document signing system with **unified Blazor Auto (Server+WASM hybrid) frontend** for both Senders and Receivers. Senders create envelopes and place signature fields. Receivers view PDFs, sign documents, export stamped PDFs.
|
Digital document signing system for senders and receivers.
|
||||||
|
|
||||||
**Primary Libraries:** DevExpress + PDF.js (PSPDFKit removed)
|
- Senders authenticate, view envelope lists, and manage envelope workflows.
|
||||||
|
- Receivers authenticate per envelope, open PDFs, create signatures, and apply them in the viewer.
|
||||||
**Receiver Architecture:**
|
- The active UI stack is `Blazor Auto` with server-side and WebAssembly render modes.
|
||||||
- Receiver authentication for `EnvelopeReceiverPage.razor` is now validated server-side.
|
- Primary UI/PDF libraries are `DevExpress` and `PDF.js`.
|
||||||
- Receiver page data is loaded directly via MediatR and distributed cache, not through the page's own API calls.
|
|
||||||
- PDF rendering in `EnvelopeReceiverPage.razor` is PDF.js-based, while `DxPdfViewer` remains the SSR-native viewer target.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Migration Notice
|
## Active Application Structure
|
||||||
|
|
||||||
**EnvelopeGenerator.ReceiverUI ? EnvelopeGenerator.WebUI Migration**
|
### Main Host
|
||||||
|
**Primary active application:** `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
The project has been migrated from pure Blazor WebAssembly (`ReceiverUI`) to **Blazor Auto (Server+WASM hybrid)** architecture (`WebUI`) to resolve DevExpress `DxPdfViewer` compatibility issues.
|
`EnvelopeGenerator.Server` is the current runtime host and contains:
|
||||||
|
- Blazor server host
|
||||||
|
- WebAssembly host integration
|
||||||
|
- API controllers
|
||||||
|
- authentication/authorization setup
|
||||||
|
- Swagger/Scalar setup
|
||||||
|
- YARP reverse proxy configuration
|
||||||
|
- DevExpress server-side services
|
||||||
|
- SQL Server distributed cache setup
|
||||||
|
|
||||||
**Reason:** DevExpress `DxPdfViewer` requires backend server-side rendering services that are NOT available in pure WebAssembly projects.
|
### Client Project
|
||||||
|
**Client UI project:** `EnvelopeGenerator.Server.Client`
|
||||||
|
|
||||||
**New Structure:**
|
This project contains:
|
||||||
- **WebUI** (Server project): Hosts server-side components, YARP proxy, DevExpress backend services
|
- WebAssembly-rendered pages
|
||||||
- **WebUI.Client** (WASM project): Client-side components, business logic, services
|
- client-side services
|
||||||
|
- client models and options
|
||||||
|
- sender and receiver login flows
|
||||||
|
|
||||||
**Migration Details:** See `MIGRATION_CONTEXT.md`
|
### Other Projects
|
||||||
|
- `EnvelopeGenerator.Application` — MediatR/CQRS handlers and business logic
|
||||||
|
- `EnvelopeGenerator.Domain` — domain models, constants, shared abstractions
|
||||||
|
- `EnvelopeGenerator.Infrastructure` — EF Core and infrastructure services
|
||||||
|
- `EnvelopeGenerator.PdfEditor` — PDF-related backend utilities
|
||||||
|
- `EnvelopeGenerator.API` — still exists in the solution, but the current merged app host is `EnvelopeGenerator.Server`
|
||||||
|
|
||||||
|
### Legacy / Do Not Touch
|
||||||
|
- `EnvelopeGenerator.Service`
|
||||||
|
- `EnvelopeGenerator.Form`
|
||||||
|
- `EnvelopeGenerator.BBTests`
|
||||||
|
- `EnvelopeGenerator.CommonServices`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Deployment Architecture
|
## Current Hosting Model
|
||||||
|
|
||||||
**Two Presentation Projects (Both Required):**
|
`EnvelopeGenerator.Server/Program.cs` currently configures:
|
||||||
|
- `AddRazorComponents()` with both interactive server and interactive WebAssembly components
|
||||||
|
- `AddControllers()` and `MapControllers()`
|
||||||
|
- JWT authentication for sender and receiver flows
|
||||||
|
- cookie authentication
|
||||||
|
- authorization policies using `AuthScheme.Sender`, `AuthScheme.Receiver`, `AuthPolicy.Sender`, `AuthPolicy.Receiver`
|
||||||
|
- `AddReverseProxy()` with `yarp.json`
|
||||||
|
- Swagger / OpenAPI / Scalar
|
||||||
|
- distributed SQL Server cache
|
||||||
|
- DevExpress Blazor and DevExpress PDF Viewer server-side services
|
||||||
|
- request localization middleware
|
||||||
|
|
||||||
1. **EnvelopeGenerator.API** (ASP.NET Core Web API)
|
This means the active app is a **merged UI + API host**.
|
||||||
- Runs independently (development & production)
|
|
||||||
- Backend services for document management, authentication, signature endpoints
|
|
||||||
- Serves as API endpoint for WebUI
|
|
||||||
|
|
||||||
2. **EnvelopeGenerator.WebUI** (Blazor Auto - Server+WASM Hybrid)
|
---
|
||||||
- **Server Project (`EnvelopeGenerator.WebUI`):**
|
|
||||||
- **YARP Reverse Proxy** configured via `yarp.json`
|
|
||||||
- Proxies `/api/*` requests to `API:8088`
|
|
||||||
- Hosts server-side components (`@rendermode InteractiveServer`)
|
|
||||||
- DevExpress server-side services (DxPdfViewer backend)
|
|
||||||
- **Client Project (`EnvelopeGenerator.WebUI.Client`):**
|
|
||||||
- Client-side components (`@rendermode InteractiveWebAssembly`)
|
|
||||||
- Business logic services (AuthService, DocumentService, etc.)
|
|
||||||
- WASM runtime
|
|
||||||
|
|
||||||
**Request Flow:**
|
## Reverse Proxy
|
||||||
```
|
|
||||||
Client ? WebUI:XXXX (Blazor Auto)
|
**Config file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
?? Server-side Pages (DxPdfViewer)
|
|
||||||
?? Client-side Pages (WASM)
|
Current YARP usage is focused on **AuthHub forwarding**, not a general `/api/* -> EnvelopeGenerator.API` proxy.
|
||||||
?? YARP Proxy: /api/* ? API:8088
|
|
||||||
|
Configured routes forward:
|
||||||
|
- `POST /api/auth` -> AuthHub `/api/auth/sign-flow`
|
||||||
|
- `POST /api/Auth/envelope-receiver/{key}` -> AuthHub `/api/auth/envelope-receiver/{key}?cookie=true`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Active Routes and Files
|
||||||
|
|
||||||
|
### WebAssembly Pages (`EnvelopeGenerator.Server.Client`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/IndexPage.razor` | WebAssembly | Landing page |
|
||||||
|
| `/sender/login` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginSenderPage.razor` | WebAssembly | Sender login |
|
||||||
|
| `/sender` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor` | WebAssembly (`prerender: false`) | Sender dashboard |
|
||||||
|
| `/envelope/login/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` | WebAssembly | Receiver login |
|
||||||
|
|
||||||
|
### Server Pages (`EnvelopeGenerator.Server`)
|
||||||
|
| Route | File | Render Mode | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/envelope/{EnvelopeKey}` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` | InteractiveServer | Main receiver PDF viewer and signing page |
|
||||||
|
| `/envelope/DxPdfViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | InteractiveServer | DevExpress PDF Viewer test page |
|
||||||
|
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | InteractiveServer | DevExpress report-based PDF rendering |
|
||||||
|
| `/envelope/Embed` | `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage_embed.razor` | InteractiveServer | Embedded browser PDF view test page |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current API Location
|
||||||
|
|
||||||
|
The active application exposes controllers from:
|
||||||
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers`
|
||||||
|
|
||||||
|
Current controller set includes:
|
||||||
|
- `AnnotationController`
|
||||||
|
- `AuthController`
|
||||||
|
- `CacheController`
|
||||||
|
- `ConfigController`
|
||||||
|
- `DocumentController`
|
||||||
|
- `EmailTemplateController`
|
||||||
|
- `EnvelopeController`
|
||||||
|
- `EnvelopeReceiverController`
|
||||||
|
- `EnvelopeTypeController`
|
||||||
|
- `HistoryController`
|
||||||
|
- `LocalizationController`
|
||||||
|
- `ReadOnlyController`
|
||||||
|
- `ReceiverController`
|
||||||
|
- `SignatureController`
|
||||||
|
- `TfaRegistrationController`
|
||||||
|
|
||||||
|
Do not assume API behavior lives only in `EnvelopeGenerator.API`; the active merged host contains controller endpoints directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Authentication Model
|
||||||
|
|
||||||
|
### Sender
|
||||||
|
Client login page uses `EnvelopeGenerator.Server.Client/Services/AuthService.cs`.
|
||||||
|
|
||||||
|
Key sender endpoints:
|
||||||
|
- `POST /api/auth?cookie=true` — login
|
||||||
|
- `GET /api/auth/check` — current sender access check
|
||||||
|
- `POST /api/auth/logout` — logout
|
||||||
|
|
||||||
|
### Receiver
|
||||||
|
Receiver authentication is **per envelope**.
|
||||||
|
|
||||||
|
Key receiver endpoints used by client services:
|
||||||
|
- `POST /api/Auth/envelope-receiver/{envelopeKey}` — submit access code
|
||||||
|
- `GET /api/auth/check/envelope/{envelopeKey}` — check access
|
||||||
|
- `POST /api/auth/logout/envelope/{envelopeKey}` — logout receiver for one envelope
|
||||||
|
|
||||||
|
Receiver cookie resolution in server auth uses an envelope-specific cookie name derived from:
|
||||||
|
- `AuthTokenSignFLOWReceiver.{envelopeKey}` pattern
|
||||||
|
|
||||||
|
### Receiver Server-Side Authorization
|
||||||
|
`EnvelopeReceiverPage.razor` does **not** rely on its own API access-check call for page authorization.
|
||||||
|
|
||||||
|
It uses:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- tries the current `HttpContext.User`
|
||||||
|
- if needed, reads the per-envelope receiver cookie directly
|
||||||
|
- validates the JWT with the receiver auth scheme
|
||||||
|
- verifies the token subject matches the route envelope key
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver Page Data Loading
|
||||||
|
|
||||||
|
Main server-side page data service:
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
This service loads directly via MediatR and distributed cache:
|
||||||
|
- document bytes
|
||||||
|
- receiver envelope data
|
||||||
|
- signature placeholders
|
||||||
|
- cached signature data
|
||||||
|
|
||||||
|
For signature placeholders, the service:
|
||||||
|
- reads document receiver elements
|
||||||
|
- filters them for the authenticated receiver
|
||||||
|
- converts coordinates to `UnitOfLength.Point` before UI use
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Receiver PDF Viewer
|
||||||
|
|
||||||
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
||||||
|
|
||||||
|
Current receiver viewer characteristics:
|
||||||
|
- route: `/envelope/{EnvelopeKey}`
|
||||||
|
- render mode: `InteractiveServer`
|
||||||
|
- 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
|
||||||
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
DocumentContent="@_pdfDocumentContent"
|
||||||
|
ZoomLevel="@_viewerZoomLevel"
|
||||||
|
IsSinglePagePreview="true"
|
||||||
|
CustomizeToolbar="OnCustomizeToolbar" />
|
||||||
```
|
```
|
||||||
|
|
||||||
**Configuration:** `EnvelopeGenerator.WebUI/yarp.json`
|
```csharp
|
||||||
|
// ZoomLevel: always divide by 100 (factor, not percentage)
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d; // 150 -> 1.5
|
||||||
|
|
||||||
---
|
// PageCount: read directly, no JS needed
|
||||||
|
_totalPages = _pdfViewer.PageCount;
|
||||||
|
|
||||||
## WebUI Route Structure
|
// Page navigation: only via CustomizeToolbar buttons
|
||||||
|
protected void OnCustomizeToolbar(ToolbarModel toolbarModel)
|
||||||
### Root Route
|
|
||||||
| Route | File | Location | Render Mode |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `/` | `Index.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
|
||||||
|
|
||||||
### Sender Routes
|
|
||||||
| Route | File | Location | Render Mode |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `/sender/login` | `LoginSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
|
||||||
| `/sender` | `EnvelopeSenderPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
|
||||||
|
|
||||||
### Receiver Routes (PDF Viewers)
|
|
||||||
| Route | File | Location | Render Mode |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `/envelope/login/{EnvelopeKey}` | `LoginReceiverPage.razor` | `WebUI.Client/Pages/` | `@rendermode InteractiveWebAssembly` |
|
|
||||||
| `/envelope/{EnvelopeKey}` | `EnvelopeReceiverPage.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/DxPdfViewer` | `EnvelopeReceiverPage_DxPdfViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/{EnvelopeKey}/DxReportViewer` | `EnvelopeReceiverPage_DxReportViewer.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
| `/envelope/Embed` | `EnvelopeReceiverPage_embed.razor` | `WebUI/Components/Pages/` | `@rendermode InteractiveServer` |
|
|
||||||
|
|
||||||
**Multi-Envelope Support:** Receivers can login to multiple envelopes simultaneously (per-envelope cookie authentication).
|
|
||||||
|
|
||||||
**Render Mode Strategy:**
|
|
||||||
- **Client-side Pages (WASM):** Login, Sender dashboard, Index (no DevExpress backend required)
|
|
||||||
- **Server-side Pages (Server):** PDF viewers (DevExpress DxPdfViewer requires backend)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architecture Evolution
|
|
||||||
|
|
||||||
### Old Architecture (Deprecated v1)
|
|
||||||
- **Sender UI:** `EnvelopeGenerator.Web` (Razor Pages + PSPDFKit)
|
|
||||||
- **Receiver UI:** Separate project
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
|
|
||||||
### Intermediate Architecture (Deprecated v2)
|
|
||||||
- **Unified Frontend:** `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM)
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
- **Issue:** DevExpress `DxPdfViewer` displayed blank screen (no backend services in WASM)
|
|
||||||
|
|
||||||
### Current Architecture (Active)
|
|
||||||
- **Frontend:** `EnvelopeGenerator.WebUI` (Blazor Auto - Server+WASM Hybrid)
|
|
||||||
- **WebUI** (Server): Server-side components, YARP proxy, DevExpress backend
|
|
||||||
- **WebUI.Client** (WASM): Client-side components, services, business logic
|
|
||||||
- **Backend:** `EnvelopeGenerator.API`
|
|
||||||
- **Libraries:** DevExpress + PDF.js
|
|
||||||
- **PSPDFKit:** **REMOVED**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution Structure
|
|
||||||
|
|
||||||
| Project | Target | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `EnvelopeGenerator.API` | net8.0 | ASP.NET Core Web API. Backend for **both Senders & Receivers**. Auth, PDF serving, signature endpoints. |
|
|
||||||
| `EnvelopeGenerator.WebUI` | net8.0 | **Blazor Auto Server Project**. YARP proxy, server-side components, DevExpress backend services. |
|
|
||||||
| `EnvelopeGenerator.WebUI.Client` | net8.0 WASM | **Blazor Auto Client Project**. Client-side components, services, business logic. |
|
|
||||||
| `EnvelopeGenerator.ReceiverUI` | net8.0 WASM | **DEPRECATED.** Pure Blazor WASM (migrated to WebUI). |
|
|
||||||
| `EnvelopeGenerator.Web` | net7/8/9 | **DEPRECATED.** Legacy Razor Pages (Sender UI). No longer used. |
|
|
||||||
| `EnvelopeGenerator.Application` | multi | MediatR CQRS handlers. Business logic. |
|
|
||||||
| `EnvelopeGenerator.Domain` | multi | Domain models, constants, interfaces. |
|
|
||||||
| `EnvelopeGenerator.Infrastructure` | multi | EF Core repos, DB context. |
|
|
||||||
| `EnvelopeGenerator.PdfEditor` | multi | iText7 utilities (NOT used in WebUI). |
|
|
||||||
| `EnvelopeGenerator.DependencyInjection` | multi | DI registration helpers. |
|
|
||||||
| **VB.NET projects** (Service/Form/BBTests) | net462 | **Legacy. Do NOT touch.** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Localization & Culture Management
|
|
||||||
|
|
||||||
**Current Architecture:** Blazor WebAssembly (client-side culture management)
|
|
||||||
|
|
||||||
### Implementation Details
|
|
||||||
|
|
||||||
**Culture Storage:**
|
|
||||||
- Culture preference stored in browser's `localStorage` (key: `AppCulture`)
|
|
||||||
- Managed by `CultureService.cs` (ReceiverUI/Services)
|
|
||||||
- Supported cultures: `de-DE`, `en-US`, `fr-FR`
|
|
||||||
|
|
||||||
**Culture Initialization:**
|
|
||||||
- **Location:** `Program.cs` (lines 53-57)
|
|
||||||
- Sets `CultureInfo.DefaultThreadCurrentCulture/UICulture` **before** app runs
|
|
||||||
- **WASM-Safe:** Each user has isolated browser instance
|
|
||||||
|
|
||||||
**Language Selector:**
|
|
||||||
- **Component:** `LanguageSelector.razor` (ReceiverUI/Shared)
|
|
||||||
- Displays flag icon + language name
|
|
||||||
- Changes culture via `CultureService.SetCultureAsync()`
|
|
||||||
- Navigates with `forceLoad: false` (smooth transition, no page reload)
|
|
||||||
|
|
||||||
### ⚠️ MIGRATION WARNING: Blazor Server/Auto
|
|
||||||
|
|
||||||
**Current approach is WASM-specific and will break in Server/Auto render modes!**
|
|
||||||
|
|
||||||
**Why it breaks:**
|
|
||||||
- `Program.cs:53-57` sets **global** `DefaultThreadCurrentCulture`
|
|
||||||
- In Server/Auto, one app instance serves **all users**
|
|
||||||
- User A selects German → User B sees German too (shared state)
|
|
||||||
- Thread-safety issues and culture conflicts
|
|
||||||
|
|
||||||
**Migration Checklist (when moving to Server/Auto):**
|
|
||||||
|
|
||||||
1. **Remove global culture initialization** from `Program.cs` (lines 53-57)
|
|
||||||
- See detailed warning comment in the code
|
|
||||||
|
|
||||||
2. **Add RequestLocalizationMiddleware** (Server-side approach):
|
|
||||||
```csharp
|
|
||||||
app.UseRequestLocalization(options => {
|
|
||||||
options.SupportedCultures = new[] { "de-DE", "en-US", "fr-FR" };
|
|
||||||
options.SupportedUICultures = options.SupportedCultures;
|
|
||||||
options.RequestCultureProviders.Insert(0, new CookieRequestCultureProvider());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **OR** Use **per-circuit culture** (Blazor Server approach):
|
|
||||||
- Store culture in circuit-scoped service
|
|
||||||
- Use `CascadingParameter` to distribute to components
|
|
||||||
- See: https://learn.microsoft.com/aspnet/core/blazor/globalization-localization
|
|
||||||
|
|
||||||
4. **Update `LanguageSelector.razor`:**
|
|
||||||
- Remove manual `CultureInfo.DefaultThreadCurrentCulture` assignment
|
|
||||||
- Use middleware/circuit culture provider instead
|
|
||||||
|
|
||||||
5. **Update `CultureService.cs`:**
|
|
||||||
- Integrate with Server-side culture provider
|
|
||||||
- May need to store in cookies instead of localStorage
|
|
||||||
|
|
||||||
**References:**
|
|
||||||
- Microsoft Docs: [Blazor Globalization/Localization](https://learn.microsoft.com/aspnet/core/blazor/globalization-localization)
|
|
||||||
- Current implementation: `Program.cs`, `CultureService.cs`, `LanguageSelector.razor`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Key Files & Routes
|
|
||||||
|
|
||||||
### Client-Side Pages (WebUI.Client)
|
|
||||||
| File | Route | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `WebUI.Client/Pages/Index.razor` | `/` | Application entry point (landing page). |
|
|
||||||
| `WebUI.Client/Pages/EnvelopeSenderPage.razor` | `/sender` | Sender dashboard (envelope list). |
|
|
||||||
| `WebUI.Client/Pages/LoginSenderPage.razor` | `/sender/login` | Sender username/password auth. |
|
|
||||||
| `WebUI.Client/Pages/LoginReceiverPage.razor` | `/envelope/login/{EnvelopeKey}` | Receiver access code auth. |
|
|
||||||
|
|
||||||
### Server-Side Pages (WebUI)
|
|
||||||
| File | Route | Purpose |
|
|
||||||
|---|---|---|
|
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage.razor` | `/envelope/{key}` | Receiver PDF viewer & signing page. Uses Interactive Server, server-side auth/data loading, and currently renders with PDF.js overlay logic. |
|
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxPdfViewer.razor` | `/envelope/DxPdfViewer` | DevExpress PDF Viewer (test page). |
|
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_DxReportViewer.razor` | `/envelope/{key}/DxReportViewer` | DevExpress Report Viewer. |
|
|
||||||
| `WebUI/Components/Pages/EnvelopeReceiverPage_embed.razor` | `/envelope/Embed` | Embedded PDF viewer (iframe). |
|
|
||||||
|
|
||||||
### Services & Assets
|
|
||||||
| File | Purpose |
|
|
||||||
|---|---|
|
|
||||||
| `WebUI.Client/Services/AuthService.cs` | Receiver + Sender authentication. |
|
|
||||||
| `WebUI.Client/Services/SignatureCacheService.cs` | Signature caching (Redis/SQL). |
|
|
||||||
| `WebUI.Client/Services/DocumentService.cs` | PDF document retrieval. |
|
|
||||||
| `WebUI/Services/EnvelopeReceiverAuthorizationService.cs` | Server-side receiver authorization for `EnvelopeReceiverPage.razor` using per-envelope cookie/JWT validation. |
|
|
||||||
| `WebUI/Services/EnvelopeReceiverPageDataService.cs` | Server-side document/signature/receiver data loading via MediatR and distributed cache. |
|
|
||||||
| `WebUI/wwwroot/js/pdf-viewer.js` | PDF.js wrapper (zoom, pagination, thumbnails). |
|
|
||||||
| `WebUI/wwwroot/js/receiver-signature.js` | Signature pad (draw/type/image). |
|
|
||||||
| `WebUI/wwwroot/css/envelope-viewer.css` | EnvelopeViewer styles. |
|
|
||||||
| `API/Controllers/CacheController.cs` | Signature cache endpoints. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Coordinate System — CRITICAL
|
|
||||||
|
|
||||||
**Database Format:** INCHES (GdPicture14 native)
|
|
||||||
**Origin:** Top-left corner
|
|
||||||
**Axes:** X right, Y down
|
|
||||||
|
|
||||||
### Conversion Formulas
|
|
||||||
|
|
||||||
| From INCHES to | Formula | Example |
|
|
||||||
|---|---|---|
|
|
||||||
| **DevExpress DX** | `x_DX = x_inches * 100` | 1.5" ? 150 DX |
|
|
||||||
| **PDF Points** | `x_pt = x_inches * 72` | 1.5" ? 108 pt |
|
|
||||||
| **PDF.js Pixels** | Normalize ? scale | `(x_inches / pageWidth) * canvasWidth * scale` |
|
|
||||||
|
|
||||||
**A4 Dimensions:**
|
|
||||||
- Width: 8.27" = 595pt = 827 DX
|
|
||||||
- Height: 11.69" = 842pt = 1169 DX
|
|
||||||
|
|
||||||
### Unit Systems
|
|
||||||
|
|
||||||
| System | Unit | Origin | Y-Axis |
|
|
||||||
|---|---|---|---|
|
|
||||||
| **Database (GdPicture14)** | Inches | Top-left | Down |
|
|
||||||
| PDF.js | Pixels | Top-left | Down |
|
|
||||||
| iText7 PDF | Points (1/72") | **Bottom-left** | **Up** (flip required) |
|
|
||||||
| ~~PSPDFKit~~ | ~~Points~~ | ~~Top-left~~ | **REMOVED** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## EnvelopeReceiver — PDF.js Viewer & Signing
|
|
||||||
|
|
||||||
**Route:** `/envelope/{EnvelopeKey}`
|
|
||||||
**Tech:** PDF.js 3.11.174 + Blazor Server (`@rendermode InteractiveServer`) + server-side auth/data loading + configurable quality
|
|
||||||
**File:** `WebUI/Components/Pages/EnvelopeReceiverPage.razor`
|
|
||||||
|
|
||||||
### Current Server-Side Loading Model
|
|
||||||
|
|
||||||
- Authorization is performed inside the server project via `EnvelopeReceiverAuthorizationService`.
|
|
||||||
- The page no longer relies on `GET /api/auth/check/envelope/{EnvelopeKey}` for its own access check.
|
|
||||||
- Document bytes, receiver data, and signature placeholders are loaded directly through MediatR using `EnvelopeReceiverPageDataService`.
|
|
||||||
- Cached signatures are loaded from distributed cache directly in the server project.
|
|
||||||
|
|
||||||
### Key Features
|
|
||||||
1. HiDPI/Retina support (4x quality)
|
|
||||||
2. Configurable quality (`appsettings.json`)
|
|
||||||
3. Unlimited zoom (50%-300%)
|
|
||||||
4. Ctrl+Wheel global zoom
|
|
||||||
5. Resizable thumbnail sidebar (150-400px, localStorage)
|
|
||||||
6. Responsive (desktop/mobile)
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
**File:** `WebUI/wwwroot/appsettings.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
{
|
||||||
"PdfViewer": {
|
toolbarModel.AllItems.Clear();
|
||||||
"ThumbnailBaseScale": 0.75,
|
var nextButton = new ToolbarItem
|
||||||
"ThumbnailEnableHiDPI": true,
|
{
|
||||||
"MainCanvasEnableHiDPI": true,
|
IconCssClass = "dx-icon-chevronnext",
|
||||||
"ZoomStepPercentage": 5
|
Enabled = _currentPage < _totalPages,
|
||||||
}
|
Click = async (args) =>
|
||||||
|
{
|
||||||
|
_currentPage++;
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
toolbarModel.AllItems.Add(nextButton);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### JavaScript API
|
**JavaScript role after migration:**
|
||||||
**File:** `WebUI/wwwroot/js/pdf-viewer.js`
|
- Overlay geometry calculations only
|
||||||
|
- Thumbnail rendering via PDF.js helper
|
||||||
|
- Custom UI interactions (sidebar resize, signature canvas)
|
||||||
|
- **NOT** for controlling DxPdfViewer page or zoom
|
||||||
|
|
||||||
```javascript
|
See `DEVEXPRESS_V25_LIMITATIONS.md` for complete verified API reference.
|
||||||
window.pdfViewer = {
|
|
||||||
initialize(canvasId, pdfDataUrl, dotNetRef),
|
### JS Assets
|
||||||
renderPage(num),
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/pdf-viewer.js`
|
||||||
renderSignatureButtons(signatures, pageNum, dotNetRef),
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/js/receiver-signature.js`
|
||||||
applySignature(signatureId, dataUrl, fullName, position, place),
|
|
||||||
zoomIn(), zoomOut(), dispose()
|
### CSS
|
||||||
}
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/wwwroot/css/envelope-viewer.css`
|
||||||
```
|
|
||||||
|
### PDF.js CDN
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
||||||
|
- `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Workflow — EnvelopeReceiver
|
## Signature Workflow
|
||||||
|
|
||||||
**IMPORTANT:** iText7 NOT used (GPL license issue). Client-side overlay system only.
|
Receiver signatures are handled as a **viewer overlay workflow**.
|
||||||
|
|
||||||
### Workflow Steps
|
### Current behavior
|
||||||
|
1. Server-side authorization validates receiver access.
|
||||||
|
2. The page loads document bytes, receiver data, signature placeholders, and cached signature state.
|
||||||
|
3. If no cached signature exists, the signature popup opens automatically.
|
||||||
|
4. Receiver creates signature using one of three tabs:
|
||||||
|
- draw
|
||||||
|
- text
|
||||||
|
- image
|
||||||
|
5. Required metadata:
|
||||||
|
- full name
|
||||||
|
- place
|
||||||
|
6. Optional metadata:
|
||||||
|
- position
|
||||||
|
7. Clicking a signature placeholder applies the signature as a client-side overlay in the PDF viewer.
|
||||||
|
|
||||||
1. **Page Load:**
|
### Important note
|
||||||
- Validate receiver access server-side using the per-envelope auth cookie
|
Although `itext` is referenced by the server project, the current receiver page signing flow is **not PDF stamping-based**. The active receiver UI uses client-side overlay behavior in the viewer.
|
||||||
- Load document, receiver, and signature data directly through MediatR
|
|
||||||
- Check distributed cache for cached signature
|
|
||||||
- If cached ? skip popup, load signature
|
|
||||||
- If not ? show automatic popup (mandatory)
|
|
||||||
|
|
||||||
2. **Signature Popup (DxPopup):**
|
### Signature DTO
|
||||||
- **Cannot close** (no X, no ESC, no outside-click)
|
`EnvelopeGenerator.Server.Client/Models/SignatureCaptureDto.cs`
|
||||||
- **3 Tabs:** Draw (canvas) / Text (font select) / Image (upload)
|
|
||||||
- **Required:** Full name, Place
|
|
||||||
- **Optional:** Position
|
|
||||||
- **Save ?** Store in `_capturedSignature`, cache via API
|
|
||||||
|
|
||||||
3. **Signature Buttons:**
|
|
||||||
- Render purple "Unterschreiben" buttons at signature field positions
|
|
||||||
- Coordinates: INCHES ? POINTS ? Pixels (scaled)
|
|
||||||
- File: `pdf-viewer.js` ? `renderSignatureButtons()`
|
|
||||||
|
|
||||||
4. **Apply Signature (Click "Unterschreiben"):**
|
|
||||||
- JS: Remove button, create HTML overlay
|
|
||||||
- Format: Image + separator + text (Name, Position, Place, Date)
|
|
||||||
- **NOT stamped on PDF bytes** (visual overlay only)
|
|
||||||
|
|
||||||
5. **Re-rendering:**
|
|
||||||
- Zoom/Page change ? recalculate button positions
|
|
||||||
- Session state: `_capturedSignature` (lost on refresh)
|
|
||||||
|
|
||||||
### Authentication Notes
|
|
||||||
|
|
||||||
- Receiver cookies are stored per envelope: `AuthTokenSignFLOWReceiver.{envelopeKey}`.
|
|
||||||
- `EnvelopeReceiverPage.razor` uses server-side receiver authorization logic instead of calling its own auth check API endpoint.
|
|
||||||
- The server-side auth flow must remain compatible with `AuthScheme.Receiver` and `AuthPolicy.Receiver`.
|
|
||||||
|
|
||||||
### Data Model
|
|
||||||
**File:** `WebUI.Client/Models/SignatureCaptureDto.cs`
|
|
||||||
|
|
||||||
```csharp
|
```csharp
|
||||||
public sealed record SignatureCaptureDto {
|
public sealed record SignatureCaptureDto {
|
||||||
public required string DataUrl { get; init; } // base64 PNG
|
public required string DataUrl { get; init; }
|
||||||
public required string FullName { get; init; }
|
public required string FullName { get; init; }
|
||||||
public string Position { get; init; } = ""; // Optional
|
public string Position { get; init; } = "";
|
||||||
public required string Place { get; init; }
|
public required string Place { get; init; }
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Signature Caching
|
## Signature Cache
|
||||||
|
|
||||||
**Purpose:** Persist signature across page refreshes (distributed cache: Redis/SQL)
|
### Active cache model
|
||||||
|
The current receiver page cache flow is handled directly in the server project through:
|
||||||
|
- `EnvelopeReceiverPageDataService`
|
||||||
|
- `IDistributedCache`
|
||||||
|
- SQL Server distributed cache configuration from `Program.cs`
|
||||||
|
|
||||||
### API Endpoints
|
### Cache key format
|
||||||
**Controller:** `API/Controllers/CacheController.cs`
|
Current server-side key prefix:
|
||||||
|
- `envelope-generator.receiver-ui.signature:{receiverSignature}`
|
||||||
|
|
||||||
- `POST /api/Cache/SignatureCapture/{envelopeKey}` — Save
|
This is different from an envelope-key-only cache convention.
|
||||||
- `GET /api/Cache/SignatureCapture/{envelopeKey}` — Load
|
|
||||||
- `DELETE /api/Cache/SignatureCapture/{envelopeKey}` — Delete
|
|
||||||
|
|
||||||
**Cache Key Format:**
|
### Config
|
||||||
```
|
`EnvelopeGenerator.Server/EnvelopeGenerator.Server/Options/CacheOptions.cs`
|
||||||
signature:91751687-8ae6-4777-bf5f-b8846085e62e:{envelopeKey}
|
- section name: `Cache`
|
||||||
```
|
- option: `SignatureCacheExpiration`
|
||||||
|
|
||||||
**Configuration:** `appsettings.json`
|
### Related controller
|
||||||
```json
|
A cache API controller also exists in:
|
||||||
{
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/CacheController.cs`
|
||||||
"Cache": {
|
|
||||||
"SignatureCacheExpiration": null // or "02:00:00" for 2h
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Service
|
|
||||||
**File:** `WebUI.Client/Services/SignatureCacheService.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
public class SignatureCacheService {
|
|
||||||
Task SaveSignatureAsync(string envelopeKey, SignatureCaptureDto signature);
|
|
||||||
Task<SignatureCaptureDto?> GetSignatureAsync(string envelopeKey);
|
|
||||||
Task DeleteSignatureAsync(string envelopeKey);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Error Handling:** Fire-and-forget saves, graceful degradation on load failure.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Sender Login
|
## Sender Dashboard
|
||||||
|
|
||||||
**Route:** `/sender/login`
|
**Main file:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/EnvelopeSenderPage.razor`
|
||||||
**File:** `WebUI.Client/Pages/LoginSenderPage.razor`
|
|
||||||
**Tech:** Bootstrap 5 + DevExpress Blazing Berry theme
|
|
||||||
|
|
||||||
### AuthService Extension
|
Current behavior:
|
||||||
**File:** `WebUI.Client/Services/AuthService.cs`
|
- checks sender access through `AuthService.CheckSenderAccessAsync()`
|
||||||
|
- redirects to `/sender/login` when unauthorized
|
||||||
|
- loads envelope list through client `EnvelopeService`
|
||||||
|
- separates envelopes into active/completed tabs
|
||||||
|
- uses `DevExpress DxGrid`
|
||||||
|
|
||||||
```csharp
|
The sender page is active, but create/edit/delete actions are still marked with TODO behavior in the UI page.
|
||||||
public enum SenderLoginResult { Success, InvalidCredentials, Error }
|
|
||||||
|
|
||||||
public async Task<SenderLoginResult> LoginSenderAsync(string username, string password) {
|
|
||||||
var response = await http.PostAsJsonAsync(
|
|
||||||
$"{_api.BaseUrl}/api/auth?cookie=true",
|
|
||||||
new { username, password });
|
|
||||||
|
|
||||||
return response.StatusCode switch {
|
|
||||||
HttpStatusCode.OK => SenderLoginResult.Success,
|
|
||||||
HttpStatusCode.Unauthorized => SenderLoginResult.InvalidCredentials,
|
|
||||||
_ => SenderLoginResult.Error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
**Endpoint:** `POST /api/auth?cookie=true`
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{ "username": "TekH", "password": "***" }
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `200 OK` ? Cookie set, redirect to `/sender`
|
|
||||||
- `401 Unauthorized` ? Show error: "Ungültige Anmeldedaten"
|
|
||||||
- Other ? Show error: "Serverfehler"
|
|
||||||
|
|
||||||
**Cookie:** HTTP-only, Secure (HTTPS), SameSite=Strict
|
|
||||||
|
|
||||||
### UI Flow
|
|
||||||
1. User enters username + password
|
|
||||||
2. Click "Anmelden" or press Enter
|
|
||||||
3. Call `AuthService.LoginSenderAsync()`
|
|
||||||
4. Success ? `Navigation.NavigateTo("/sender", forceLoad: true)`
|
|
||||||
5. Error ? Display alert
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Receiver Login
|
## Localization
|
||||||
|
|
||||||
**Route:** `/envelope/login/{EnvelopeKey}`
|
Current server host localization setup in `Program.cs`:
|
||||||
**File:** `WebUI.Client/Pages/LoginReceiverPage.razor`
|
- supported cultures: `de-DE`, `en-US`
|
||||||
|
- request localization middleware is enabled
|
||||||
|
- `QueryStringRequestCultureProvider` is added
|
||||||
|
- cookie-based localization services are registered via `AddCookieBasedLocalizer()`
|
||||||
|
|
||||||
**Multi-Envelope Support:** Cookies are stored per-envelope (e.g., `AuthTokenSignFLOWReceiver.{envelopeKey}`), allowing simultaneous authentication for multiple envelopes in the same browser session.
|
Do not assume the old ReceiverUI-only `localStorage` culture approach is the current source of truth for the active host.
|
||||||
|
|
||||||
### AuthService Method
|
|
||||||
```csharp
|
|
||||||
public enum EnvelopeLoginResult { Success, InvalidCode, NotFound, Error }
|
|
||||||
|
|
||||||
public async Task<EnvelopeLoginResult> LoginEnvelopeReceiverAsync(string key, string accessCode) {
|
|
||||||
var form = new MultipartFormDataContent();
|
|
||||||
form.Add(new StringContent(accessCode), "AccessCode");
|
|
||||||
|
|
||||||
var response = await http.PostAsync(
|
|
||||||
$"{_api.BaseUrl}/api/Auth/envelope-receiver/{Uri.EscapeDataString(key)}", form);
|
|
||||||
|
|
||||||
return response.StatusCode switch {
|
|
||||||
HttpStatusCode.OK => EnvelopeLoginResult.Success,
|
|
||||||
HttpStatusCode.Unauthorized => EnvelopeLoginResult.InvalidCode,
|
|
||||||
HttpStatusCode.NotFound => EnvelopeLoginResult.NotFound,
|
|
||||||
_ => EnvelopeLoginResult.Error
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Success:** Redirect to `/envelope/{key}`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## NuGet Packages (WebUI.Client)
|
## Coordinate System
|
||||||
|
|
||||||
| Package | Version | Purpose |
|
### Source data
|
||||||
|---|---|---|
|
Database signature coordinates are still based on:
|
||||||
| `DevExpress.Blazor.*` | 25.2.3 | UI components (grids, popups, etc.) |
|
- **unit:** inches
|
||||||
| `SkiaSharp.*` | 3.119.1 | WASM rendering |
|
- **origin:** top-left
|
||||||
| ~~`itext`~~ | ~~8.0.5~~ | **NOT USED** (GPL license) |
|
- **axes:** X right, Y down
|
||||||
|
|
||||||
**External CDN:**
|
### Relevant conversions
|
||||||
- PDF.js 3.11.174: `https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js`
|
- inches -> PDF points: `x_pt = x_inches * 72`
|
||||||
|
- inches -> DevExpress DX units: `x_dx = x_inches * 100`
|
||||||
|
|
||||||
|
### Current receiver page behavior
|
||||||
|
The server page data service converts signature placeholders to **points** before sending them into the viewer workflow.
|
||||||
|
|
||||||
|
### Unit systems to keep in mind
|
||||||
|
| System | Unit | Origin | Y-axis |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Database | Inches | Top-left | Down |
|
||||||
|
| PDF.js display | Pixels | Top-left | Down |
|
||||||
|
| PDF points | Points | Depends on PDF model | Depends on consumer |
|
||||||
|
| DevExpress DX | 1/100 inch style coordinates | Top-left-oriented usage in this app | Down-oriented usage |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Mistakes History — Do NOT Repeat
|
## Key Services and Files
|
||||||
|
|
||||||
| Mistake | Why Wrong |
|
### Client services
|
||||||
|---|---|
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs`
|
||||||
| Using iText7 in EnvelopeReceiver | GPL license issue. Use overlay system instead. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/EnvelopeService.cs`
|
||||||
| Using PSPDFKit | Removed from architecture. Use PDF.js + DevExpress. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/DocumentService.cs`
|
||||||
| Hardcoded quality values in PDF.js | Use `appsettings.json` for configurability. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/SignatureCacheService.cs`
|
||||||
| Complex toolbar layouts | User wants simplicity. Keep horizontal layout. |
|
|
||||||
| Over-designed UI (gradients/badges) | User prefers simple text labels. |
|
### Server services
|
||||||
| Ignoring "revert" instructions | Revert HTML structure, not just CSS. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
||||||
| `BottomMarginBand` for signatures | Repeats on every page. Use DetailBand. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
||||||
| `imageY = (page-1) * 1169 + ann.Y` | Inflates DetailBand. Calculate per-page. |
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverAuthorizationService.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Services/EnvelopeReceiverPageDataService.cs`
|
||||||
|
|
||||||
|
### Server config and host files
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs`
|
||||||
|
- `EnvelopeGenerator.Server/EnvelopeGenerator.Server/yarp.json`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development Notes
|
## Working Rules for This Workspace
|
||||||
|
|
||||||
### Deprecated Projects
|
- Treat `EnvelopeGenerator.Server` as the active main application host.
|
||||||
**DO NOT USE:**
|
- Treat `EnvelopeGenerator.Server.Client` as the active client UI project.
|
||||||
- `EnvelopeGenerator.ReceiverUI` (Pure Blazor WASM) — Migrated to WebUI (DevExpress compatibility issue)
|
- Prefer current `Server` / `Server.Client` paths over old `WebUI` / `ReceiverUI` references.
|
||||||
- `EnvelopeGenerator.Web` (Razor Pages) — Replaced by unified WebUI
|
- Do not use `EnvelopeGenerator.Web` or `EnvelopeGenerator.ReceiverUI` as the primary implementation target unless explicitly asked.
|
||||||
- PSPDFKit — Removed, use PDF.js + DevExpress instead
|
- Do not modify the legacy VB.NET projects unless explicitly requested.
|
||||||
|
- For receiver PDF/signature work, prefer the current `PDF.js`-based flow in `EnvelopeReceiverPage.razor`.
|
||||||
### Legacy Projects (VB.NET)
|
- For DevExpress PDF viewer issues, remember server-side services are registered in `EnvelopeGenerator.Server`.
|
||||||
**DO NOT TOUCH:** `EnvelopeGenerator.Service`, `EnvelopeGenerator.Form`, `EnvelopeGenerator.BBTests`
|
|
||||||
|
|
||||||
### Signature Coordinate Evidence
|
|
||||||
**File:** `EnvelopeGenerator.Form/frmFieldEditor.vb` (VB.NET)
|
|
||||||
|
|
||||||
```vb
|
|
||||||
Private Const SIGNATURE_WIDTH As Single = 1.77 ' inches
|
|
||||||
Private Const SIGNATURE_HEIGHT As Single = 1.96 ' inches
|
|
||||||
|
|
||||||
Sub LoadAnnotation(pElement As Signature, ...)
|
|
||||||
oAnnotation.Left = CSng(pElement.X) ' Direct INCHES assignment
|
|
||||||
oAnnotation.Top = CSng(pElement.Y)
|
|
||||||
End Sub
|
|
||||||
```
|
|
||||||
|
|
||||||
Proves database uses INCHES natively.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Quick Reference
|
**Last Updated:** 2026-06-29
|
||||||
|
|
||||||
### When working with coordinates:
|
|
||||||
1. **Database ? UI:** INCHES × 72 = PDF Points
|
|
||||||
2. **UI ? Display:** Points × scale = Pixels
|
|
||||||
3. **iText7 stamping:** Flip Y-axis (top-down ? bottom-up)
|
|
||||||
|
|
||||||
### When adding features:
|
|
||||||
1. Check `Mistakes History` first
|
|
||||||
2. Prefer simplicity over complexity
|
|
||||||
3. Use `appsettings.json` for configuration
|
|
||||||
4. Keep consistent with existing design (Bootstrap 5 + Blazing Berry)
|
|
||||||
5. **Unified frontend:** WebUI serves both Senders and Receivers
|
|
||||||
6. **Render mode:** Client-side (WASM) for login/dashboard, Server-side for PDF viewers
|
|
||||||
|
|
||||||
### When debugging:
|
|
||||||
1. **Coordinates:** Always check unit system (inches/points/pixels)
|
|
||||||
2. **Authentication:** Check cookie name/domain/SameSite
|
|
||||||
3. **Cache:** Check Redis/SQL connection + key format
|
|
||||||
4. **Frontend confusion:** Only use WebUI (ReceiverUI/Web are deprecated)
|
|
||||||
5. **Blank DxPdfViewer:** Ensure page has `@rendermode InteractiveServer`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2025-01-27 (ReceiverUI ? WebUI migration complete)
|
|
||||||
|
|||||||
300
DEBUG_NOTES.md
Normal file
300
DEBUG_NOTES.md
Normal file
@@ -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 *@
|
||||||
|
<div class="pdf-toolbar__section">
|
||||||
|
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||||
|
```
|
||||||
|
|
||||||
|
**C# Method:**
|
||||||
|
```csharp
|
||||||
|
async Task ShowDebugUI()
|
||||||
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("dxPdfViewerShowDebugUI");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. JavaScript Debug Functions
|
||||||
|
|
||||||
|
**Location:** `pdf-viewer.js`
|
||||||
|
|
||||||
|
**Functions Added:**
|
||||||
|
|
||||||
|
#### `window.dxPdfViewerDebugDOM()`
|
||||||
|
- Console-based debug function
|
||||||
|
- Logs detailed DOM analysis to browser console
|
||||||
|
- Returns analysis object for programmatic inspection
|
||||||
|
|
||||||
|
#### `window.dxPdfViewerShowDebugUI()`
|
||||||
|
- HTML overlay-based debug function
|
||||||
|
- Creates visual debug panel without console interaction
|
||||||
|
- No security warnings (no need to paste code)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Step 1: Run Application
|
||||||
|
```powershell
|
||||||
|
dotnet run --project EnvelopeGenerator.Server/EnvelopeGenerator.Server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Open Receiver Page
|
||||||
|
Navigate to: `https://localhost:8088/envelope/{EnvelopeKey}`
|
||||||
|
|
||||||
|
### Step 3: Click Debug Button
|
||||||
|
- Look for the **orange "?" button** in the PDF toolbar (left side, after thumbnails toggle)
|
||||||
|
- Click it to open the debug overlay
|
||||||
|
|
||||||
|
### Step 4: Review Debug Information
|
||||||
|
|
||||||
|
The overlay shows:
|
||||||
|
- **Total Inputs**: Number of input elements found
|
||||||
|
- **Input Elements**: Details of each input (type, class, ID, value)
|
||||||
|
- **Selector Tests**: Which CSS selectors work (✓) and which don't (✗)
|
||||||
|
- **Toolbar**: Whether toolbar element was found
|
||||||
|
- **DxWidget**: Whether DevExpress widget element was found
|
||||||
|
|
||||||
|
### Step 5: Test Page Navigation
|
||||||
|
Click the **"Test: Go to Page 2"** button in the overlay
|
||||||
|
|
||||||
|
### Step 6: Report Results
|
||||||
|
|
||||||
|
**Copy the following information:**
|
||||||
|
|
||||||
|
1. **Total Inputs**: X
|
||||||
|
2. **Input Details**: (type, className, id for each input)
|
||||||
|
3. **Selector Test Results**: (which selectors show ✓ FOUND)
|
||||||
|
4. **Test Result**: Did PDF actually navigate to page 2? (Yes/No)
|
||||||
|
5. **Console Messages**: Any errors or warnings in F12 console
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What to Look For
|
||||||
|
|
||||||
|
### ✓ Success Indicators
|
||||||
|
- At least one selector shows **✓ FOUND**
|
||||||
|
- "Test: Go to Page 2" button actually changes PDF page
|
||||||
|
- Console shows: `✓ Found page input with selector: "..."`
|
||||||
|
|
||||||
|
### ✗ Problem Indicators
|
||||||
|
- All selectors show **✗ NOT FOUND**
|
||||||
|
- "Test: Go to Page 2" does nothing
|
||||||
|
- Console shows: `✗ Page input not found`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## After Diagnosis
|
||||||
|
|
||||||
|
Once the correct selector is identified:
|
||||||
|
|
||||||
|
### 1. Update `window.dxPdfViewerGoToPage()`
|
||||||
|
Update the `selectors` array in `pdf-viewer.js` to prioritize the working selector:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const selectors = [
|
||||||
|
'WORKING_SELECTOR_HERE', // ✓ Move this to top
|
||||||
|
'input[type="number"]',
|
||||||
|
// ... rest
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Remove Debug Code
|
||||||
|
|
||||||
|
**Files to clean up:**
|
||||||
|
|
||||||
|
#### `EnvelopeReceiverPage.razor`
|
||||||
|
Remove:
|
||||||
|
```razor
|
||||||
|
@* DEBUG: DevExpress DOM Inspector *@
|
||||||
|
<div class="pdf-toolbar__section">
|
||||||
|
<button class="pdf-toolbar__btn" @onclick="ShowDebugUI" ...>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
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!**
|
||||||
231
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
231
DEVEXPRESS_V25_LIMITATIONS.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# 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 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Events
|
||||||
|
|
||||||
|
- **`CustomizeToolbar`** – Allows toolbar customization
|
||||||
|
- **`ZoomLevelChanged`** – Fires when ZoomLevel property changes (EventCallback<double>)
|
||||||
|
|
||||||
|
## Missing Events (NOT AVAILABLE in v25.2.3)
|
||||||
|
|
||||||
|
- **`PageNumberChanged`** / **`ActivePageIndexChanged`** – 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
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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<int>("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
|
||||||
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
DocumentContent="@_pdfDocumentContent"
|
||||||
|
ZoomLevel="@_viewerZoomLevel"
|
||||||
|
IsSinglePagePreview="true" />
|
||||||
|
|
||||||
|
@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, ActivePageIndexChanged, ZoomAsync, ToolbarVisible) are NOT real. Do not use.
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
@using EnvelopeGenerator.Server.Client.Options
|
@using EnvelopeGenerator.Server.Client.Options
|
||||||
@using Microsoft.JSInterop
|
@using Microsoft.JSInterop
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
|
@using DevExpress.Blazor.PdfViewer
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
@inject IOptions<PdfViewerOptions> PdfViewerOptions
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@@ -21,7 +22,6 @@
|
|||||||
|
|
||||||
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
<link href="_content/DevExpress.Blazor.Themes/blazing-berry.bs5.min.css" rel="stylesheet" />
|
||||||
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
<link href="@AppVersion.GetVersionedUrl("css/envelope-viewer.css")" rel="stylesheet" />
|
||||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf_viewer.min.css" rel="stylesheet" />
|
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
<script src="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script>
|
<script src="@AppVersion.GetVersionedUrl("js/pdf-viewer.js")"></script>
|
||||||
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
|
<script src="@AppVersion.GetVersionedUrl("js/receiver-signature.js")"></script>
|
||||||
@@ -223,27 +223,6 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pdf-toolbar__divider"></div>
|
|
||||||
|
|
||||||
<div class="pdf-toolbar__section pdf-toolbar__zoom-section">
|
|
||||||
<button class="pdf-toolbar__btn" @onclick="ZoomOut" disabled="@(_currentZoom <= 50)" title="Verkleinern">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM4 6a.5.5 0 0 0 0 1h5a.5.5 0 0 0 0-1H4z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="pdf-toolbar__zoom-slider-container">
|
|
||||||
<input type="range" class="pdf-toolbar__zoom-slider" min="50" max="300" step="@(PdfViewerOptions.Value.ZoomStepPercentage)" value="@_currentZoom" @oninput="OnZoomSliderChanged" title="@(_currentZoom)%" />
|
|
||||||
<div class="pdf-toolbar__zoom-label">@(_currentZoom)%</div>
|
|
||||||
</div>
|
|
||||||
<button class="pdf-toolbar__btn" @onclick="ZoomIn" disabled="@(_currentZoom >= 300)" title="Vergrößern">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1.007 1.007 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0zM6.5 3a.5.5 0 0 0-1 0v2.5H3a.5.5 0 0 0 0 1h2.5V9a.5.5 0 0 0 1 0V6.5H9a.5.5 0 0 0 0-1H6.5V3z" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pdf-toolbar__divider"></div>
|
|
||||||
|
|
||||||
@if (_totalSignatures > 0)
|
@if (_totalSignatures > 0)
|
||||||
{
|
{
|
||||||
<div class="pdf-toolbar__section">
|
<div class="pdf-toolbar__section">
|
||||||
@@ -348,10 +327,16 @@
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div class="pdf-canvas-wrapper">
|
<div class="pdf-canvas-wrapper">
|
||||||
<div class="pdf-page-container">
|
<div id="pdf-dx-viewer-host" class="envelope-dx-viewer-host">
|
||||||
<canvas id="pdf-canvas" class="pdf-canvas"></canvas>
|
@if (_pdfDocumentContent is not null && _pdfDocumentContent.Length > 0)
|
||||||
<div id="pdf-text-layer" class="pdf-text-layer"></div>
|
{
|
||||||
<div id="pdf-signature-layer" class="pdf-signature-layer"></div>
|
<DxPdfViewer @ref="_pdfViewer"
|
||||||
|
CssClass="envelope-dx-pdf-viewer"
|
||||||
|
DocumentContent="@_pdfDocumentContent"
|
||||||
|
ZoomLevel="@_viewerZoomLevel"
|
||||||
|
IsSinglePagePreview="true" />
|
||||||
|
}
|
||||||
|
<div id="pdf-signature-layer" class="pdf-signature-layer pdf-signature-layer--dx"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -548,13 +533,16 @@
|
|||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
string? _errorMessage;
|
string? _errorMessage;
|
||||||
string? _pdfDataUrl;
|
string? _pdfDataUrl;
|
||||||
|
byte[]? _pdfDocumentContent;
|
||||||
bool _pdfLoaded = false;
|
bool _pdfLoaded = false;
|
||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
int _totalPages = 0;
|
int _totalPages = 0;
|
||||||
int _currentZoom = 150;
|
int _currentZoom = 150;
|
||||||
|
double _viewerZoomLevel = 1.5;
|
||||||
bool _showThumbnails = true;
|
bool _showThumbnails = true;
|
||||||
bool _isLoggingOut = false;
|
bool _isLoggingOut = false;
|
||||||
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
|
DotNetObjectReference<EnvelopeReceiverPage>? _dotNetRef;
|
||||||
|
DxPdfViewer? _pdfViewer;
|
||||||
IReadOnlyList<SignatureDto> _signatures = [];
|
IReadOnlyList<SignatureDto> _signatures = [];
|
||||||
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
EnvelopeGenerator.Application.Common.Dto.EnvelopeReceiver.EnvelopeReceiverDto? _envelopeReceiver;
|
||||||
ClaimsPrincipal? _receiverUser;
|
ClaimsPrincipal? _receiverUser;
|
||||||
@@ -615,6 +603,7 @@
|
|||||||
|
|
||||||
if (pdfBytes is { Length: > 0 })
|
if (pdfBytes is { Length: > 0 })
|
||||||
{
|
{
|
||||||
|
_pdfDocumentContent = pdfBytes;
|
||||||
var base64 = Convert.ToBase64String(pdfBytes);
|
var base64 = Convert.ToBase64String(pdfBytes);
|
||||||
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
|
_pdfDataUrl = $"data:application/pdf;base64,{base64}";
|
||||||
}
|
}
|
||||||
@@ -721,17 +710,18 @@
|
|||||||
options.ZoomStepPercentage
|
options.ZoomStepPercentage
|
||||||
});
|
});
|
||||||
|
|
||||||
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", "pdf-canvas", _pdfDataUrl, _dotNetRef);
|
var success = await JSRuntime.InvokeAsync<bool>("pdfViewer.initialize", _pdfDataUrl, "pdf-dx-viewer-host", "pdf-signature-layer", _dotNetRef);
|
||||||
|
|
||||||
if (success)
|
if (success)
|
||||||
{
|
{
|
||||||
_pdfLoaded = true;
|
_pdfLoaded = true;
|
||||||
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
_totalPages = await JSRuntime.InvokeAsync<int>("pdfViewer.getTotalPages");
|
||||||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
_currentPage = 1;
|
||||||
|
|
||||||
// Attach resize listeners
|
// Attach resize listeners
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
|
await JSRuntime.InvokeVoidAsync("pdfViewer.attachResizeListeners", _dotNetRef);
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.attachViewerInteractionListeners", "pdf-dx-viewer-host", _dotNetRef);
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
@@ -754,59 +744,49 @@
|
|||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
public async Task OnZoomChanged(double scale)
|
public async Task OnZoomChanged(double scale)
|
||||||
{
|
{
|
||||||
_currentZoom = (int)(scale * 100);
|
var requestedZoom = (int)Math.Round(scale * 100, MidpointRounding.AwayFromZero);
|
||||||
await InvokeAsync(StateHasChanged);
|
await SetZoom(requestedZoom);
|
||||||
|
|
||||||
// Small delay for canvas render to complete (reduced from 100ms to 10ms)
|
|
||||||
await Task.Delay(10);
|
|
||||||
await RenderSignatureButtonsAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task NextPage()
|
async Task NextPage()
|
||||||
{
|
{
|
||||||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.nextPage"))
|
if (_currentPage >= _totalPages)
|
||||||
{
|
return;
|
||||||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
|
||||||
await RenderSignatureButtonsAsync();
|
_currentPage++;
|
||||||
}
|
await ApplyViewerStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task PreviousPage()
|
async Task PreviousPage()
|
||||||
{
|
{
|
||||||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.previousPage"))
|
if (_currentPage <= 1)
|
||||||
{
|
return;
|
||||||
_currentPage = await JSRuntime.InvokeAsync<int>("pdfViewer.getCurrentPage");
|
|
||||||
await RenderSignatureButtonsAsync();
|
_currentPage--;
|
||||||
}
|
await ApplyViewerStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task ZoomIn()
|
async Task ZoomIn()
|
||||||
{
|
{
|
||||||
if (_currentZoom >= 300) return;
|
if (_currentZoom >= 300) return;
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomIn");
|
await SetZoom(_currentZoom + PdfViewerOptions.Value.ZoomStepPercentage);
|
||||||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
|
||||||
_currentZoom = (int)(scale * 100);
|
|
||||||
|
|
||||||
// Update signature overlay positions after zoom
|
|
||||||
await RenderSignatureButtonsAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task ZoomOut()
|
async Task ZoomOut()
|
||||||
{
|
{
|
||||||
if (_currentZoom <= 50) return;
|
if (_currentZoom <= 50) return;
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.zoomOut");
|
await SetZoom(_currentZoom - PdfViewerOptions.Value.ZoomStepPercentage);
|
||||||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
|
||||||
_currentZoom = (int)(scale * 100);
|
|
||||||
|
|
||||||
// Update signature overlay positions after zoom
|
|
||||||
await RenderSignatureButtonsAsync();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task SetZoom(int percentage)
|
async Task SetZoom(int percentage)
|
||||||
{
|
{
|
||||||
var scale = percentage / 100.0;
|
_currentZoom = Math.Clamp(percentage, 50, 300);
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.setScale", scale);
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
_currentZoom = percentage;
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
|
||||||
|
await Task.Delay(150);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task OnZoomSliderChanged(ChangeEventArgs e)
|
async Task OnZoomSliderChanged(ChangeEventArgs e)
|
||||||
@@ -824,18 +804,20 @@
|
|||||||
{
|
{
|
||||||
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages)
|
if (int.TryParse(e.Value?.ToString(), out var pageNum) && pageNum >= 1 && pageNum <= _totalPages)
|
||||||
{
|
{
|
||||||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
|
_currentPage = pageNum;
|
||||||
{
|
await ApplyViewerStateAsync();
|
||||||
_currentPage = pageNum;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task FitToWidth()
|
async Task FitToWidth()
|
||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.fitToWidth");
|
_viewerZoomLevel = -2;
|
||||||
var scale = await JSRuntime.InvokeAsync<double>("pdfViewer.getScale");
|
_currentZoom = 150;
|
||||||
_currentZoom = (int)(scale * 100);
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
|
||||||
|
await Task.Delay(150);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task ToggleThumbnails()
|
async Task ToggleThumbnails()
|
||||||
@@ -853,11 +835,11 @@
|
|||||||
|
|
||||||
async Task GoToPageFromThumbnail(int pageNum)
|
async Task GoToPageFromThumbnail(int pageNum)
|
||||||
{
|
{
|
||||||
if (await JSRuntime.InvokeAsync<bool>("pdfViewer.goToPage", pageNum))
|
if (pageNum < 1 || pageNum > _totalPages)
|
||||||
{
|
return;
|
||||||
_currentPage = pageNum;
|
|
||||||
await RenderSignatureButtonsAsync();
|
_currentPage = pageNum;
|
||||||
}
|
await ApplyViewerStateAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task RenderSignatureButtonsAsync()
|
async Task RenderSignatureButtonsAsync()
|
||||||
@@ -866,6 +848,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
|
||||||
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
|
await JSRuntime.InvokeVoidAsync("pdfViewer.renderSignatureButtons", _signatures, _currentPage, _dotNetRef);
|
||||||
await UpdateSignatureCounterAsync();
|
await UpdateSignatureCounterAsync();
|
||||||
}
|
}
|
||||||
@@ -906,7 +889,13 @@
|
|||||||
public async Task OnPageChangedBySignatureNav(int newPage)
|
public async Task OnPageChangedBySignatureNav(int newPage)
|
||||||
{
|
{
|
||||||
_currentPage = newPage;
|
_currentPage = newPage;
|
||||||
await RenderSignatureButtonsAsync();
|
await ApplyViewerStateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[JSInvokable]
|
||||||
|
public async Task OnZoomGestureRequested(int zoomPercentage)
|
||||||
|
{
|
||||||
|
await SetZoom(zoomPercentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task UpdateSignatureCounterAsync()
|
async Task UpdateSignatureCounterAsync()
|
||||||
@@ -1189,6 +1178,22 @@
|
|||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async Task ApplyViewerStateAsync()
|
||||||
|
{
|
||||||
|
if (!_pdfLoaded)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_viewerZoomLevel > 0)
|
||||||
|
{
|
||||||
|
_viewerZoomLevel = _currentZoom / 100d;
|
||||||
|
}
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
await JSRuntime.InvokeVoidAsync("pdfViewer.setViewState", _currentPage, _currentZoom);
|
||||||
|
await Task.Delay(150);
|
||||||
|
await RenderSignatureButtonsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_pdfLoaded)
|
if (_pdfLoaded)
|
||||||
|
|||||||
@@ -10,3 +10,4 @@
|
|||||||
@using EnvelopeGenerator.Server.Client
|
@using EnvelopeGenerator.Server.Client
|
||||||
@using EnvelopeGenerator.Server.Components
|
@using EnvelopeGenerator.Server.Components
|
||||||
@using DevExpress.Blazor
|
@using DevExpress.Blazor
|
||||||
|
@using DevExpress.Blazor.PdfViewer
|
||||||
|
|||||||
@@ -584,6 +584,32 @@ body.resizing {
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.envelope-dx-viewer-host {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 720px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.envelope-dx-pdf-viewer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 720px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-pdf-viewer,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-pdfviewer,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-container,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-scroll-viewer,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-scroll-viewer-content,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-pdf-viewer-content,
|
||||||
|
.envelope-dx-pdf-viewer .dxbl-pdfviewer-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-page-container {
|
.pdf-page-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -641,6 +667,11 @@ body.resizing {
|
|||||||
z-index: 20;
|
z-index: 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pdf-signature-layer--dx {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.pdf-signature-layer .signature-button {
|
.pdf-signature-layer .signature-button {
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{134D4164-B29
|
|||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
COPILOT_CONTEXT.md = COPILOT_CONTEXT.md
|
||||||
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
FORM_APPLICATION_CONTEXT.md = FORM_APPLICATION_CONTEXT.md
|
||||||
OPEN_SSR_TASK.md = OPEN_SSR_TASK.md
|
RECEIVER_PDF_VIEWER_CONTEXT.md = RECEIVER_PDF_VIEWER_CONTEXT.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{0CBC2432-A561-4440-89BC-671B66A24146}"
|
||||||
|
|||||||
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
|
||||||
553
OPEN_SSR_TASK.md
553
OPEN_SSR_TASK.md
@@ -1,553 +0,0 @@
|
|||||||
# SSR Authentication Migration — Implementation Notes
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
Migration from WASM client-side authentication to SSR (Server-Side Rendering) authentication for `EnvelopeReceiverPage.razor` to fix authentication issues in Blazor InteractiveServer mode.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problem Statement
|
|
||||||
|
|
||||||
### Issue
|
|
||||||
`EnvelopeReceiverPage.razor` uses `@rendermode InteractiveServer` but was calling **WASM client service** `AuthService.CheckEnvelopeAccessAsync()`:
|
|
||||||
|
|
||||||
```razor
|
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
|
||||||
|
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why This Failed:**
|
|
||||||
- `AuthService` is a **WASM client service** that uses `IHttpClientFactory`
|
|
||||||
- In SSR context, `HttpContext` is required to configure the base address
|
|
||||||
- `CheckEnvelopeAccessAsync()` makes an HTTP request to `/api/auth/check/envelope/{key}`
|
|
||||||
- This request **goes to itself** (server calling its own endpoint), causing issues
|
|
||||||
- Returns `false` even when user is authenticated
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Solution Architecture
|
|
||||||
|
|
||||||
### Created New SSR Authentication Service
|
|
||||||
|
|
||||||
**Files Created:**
|
|
||||||
1. `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs` (Interface)
|
|
||||||
2. `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs` (Implementation)
|
|
||||||
|
|
||||||
**Purpose:** Direct `HttpContext.User` validation without HTTP requests
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### 1. IEnvelopeAuthService Interface
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.Server/Services/IEnvelopeAuthService.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace EnvelopeGenerator.Server.Services;
|
|
||||||
|
|
||||||
public interface IEnvelopeAuthService
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the current user is authenticated for the given envelope key.
|
|
||||||
/// Validates both that the user is authenticated AND that the envelope key matches their claims.
|
|
||||||
/// </summary>
|
|
||||||
bool IsAuthenticated(string envelopeKey);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the authenticated envelope key from the current user's claims (NameIdentifier or "sub" claim).
|
|
||||||
/// </summary>
|
|
||||||
string? GetAuthenticatedEnvelopeKey();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the current HttpContext user principal.
|
|
||||||
/// </summary>
|
|
||||||
ClaimsPrincipal? GetCurrentUser();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Methods:**
|
|
||||||
- `IsAuthenticated(string envelopeKey)`: Validates user auth + envelope key match
|
|
||||||
- `GetAuthenticatedEnvelopeKey()`: Extracts envelope key from claims
|
|
||||||
- `GetCurrentUser()`: Returns `ClaimsPrincipal` for advanced scenarios
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2. EnvelopeAuthService Implementation
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.Server/Services/EnvelopeAuthService.cs`
|
|
||||||
|
|
||||||
**Dependencies:**
|
|
||||||
- `IHttpContextAccessor`: Access current HTTP context
|
|
||||||
- `ILogger<EnvelopeAuthService>`: Structured logging
|
|
||||||
|
|
||||||
**Logic:**
|
|
||||||
```csharp
|
|
||||||
public bool IsAuthenticated(string envelopeKey)
|
|
||||||
{
|
|
||||||
// 1. Validate envelope key parameter
|
|
||||||
if (string.IsNullOrWhiteSpace(envelopeKey))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// 2. Get HttpContext
|
|
||||||
var context = _httpContextAccessor.HttpContext;
|
|
||||||
|
|
||||||
// 3. Check if user is authenticated
|
|
||||||
if (context?.User?.Identity?.IsAuthenticated != true)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// 4. Extract envelope key from claims
|
|
||||||
var sub = GetEnvelopeKeyFromClaims(context.User);
|
|
||||||
|
|
||||||
// 5. Verify match
|
|
||||||
return sub == envelopeKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string? GetEnvelopeKeyFromClaims(ClaimsPrincipal user)
|
|
||||||
{
|
|
||||||
// Try standard claim first
|
|
||||||
var sub = user.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
||||||
|
|
||||||
// Fallback to JWT "sub" claim
|
|
||||||
if (string.IsNullOrWhiteSpace(sub))
|
|
||||||
sub = user.FindFirst("sub")?.Value;
|
|
||||||
|
|
||||||
return sub;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Claim Priority:**
|
|
||||||
1. `ClaimTypes.NameIdentifier` (standard .NET claim)
|
|
||||||
2. `"sub"` (JWT standard claim)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3. Service Registration
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.Server/Program.cs`
|
|
||||||
|
|
||||||
**Added:**
|
|
||||||
```csharp
|
|
||||||
// SSR Authentication Service (for Envelope Receiver pages)
|
|
||||||
builder.Services.AddScoped<EnvelopeGenerator.Server.Services.IEnvelopeAuthService,
|
|
||||||
EnvelopeGenerator.Server.Services.EnvelopeAuthService>();
|
|
||||||
```
|
|
||||||
|
|
||||||
**Lifetime:** `Scoped` (per-request, matches `IHttpContextAccessor`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4. EnvelopeReceiverPage.razor Changes
|
|
||||||
|
|
||||||
**Changes Made (REVERTED - To Be Re-Applied):**
|
|
||||||
|
|
||||||
#### 4.1 Using Statements
|
|
||||||
```razor
|
|
||||||
@using EnvelopeGenerator.Server.Services
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.2 Dependency Injection
|
|
||||||
**Old:**
|
|
||||||
```razor
|
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
|
||||||
```
|
|
||||||
|
|
||||||
**New:**
|
|
||||||
```razor
|
|
||||||
@inject IEnvelopeAuthService EnvelopeAuth
|
|
||||||
@inject IHttpClientFactory HttpClientFactory
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4.3 Authentication Check in `OnInitializedAsync()`
|
|
||||||
**Old:**
|
|
||||||
```csharp
|
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
|
||||||
if (!hasAccess) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**New:**
|
|
||||||
```csharp
|
|
||||||
// ? SSR Authentication check via service
|
|
||||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Benefits:**
|
|
||||||
- ? Synchronous (no HTTP overhead)
|
|
||||||
- ? Direct `HttpContext.User` access
|
|
||||||
- ? No self-referencing HTTP calls
|
|
||||||
- ? Works in SSR context
|
|
||||||
|
|
||||||
#### 4.4 Logout Method
|
|
||||||
**Old:**
|
|
||||||
```csharp
|
|
||||||
await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey);
|
|
||||||
```
|
|
||||||
|
|
||||||
**New:**
|
|
||||||
```csharp
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// ? SSR: Direct HTTP call instead of WASM client service
|
|
||||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
|
||||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why Changed:**
|
|
||||||
- WASM `AuthService.LogoutEnvelopeReceiverAsync()` doesn't work in SSR
|
|
||||||
- Use named HttpClient `"EnvelopeGenerator.Server"` (configured in `Program.cs`)
|
|
||||||
- Graceful error handling (logout errors shouldn't block redirect)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Remaining Tasks
|
|
||||||
|
|
||||||
### ? Completed
|
|
||||||
1. ? Created `IEnvelopeAuthService` interface
|
|
||||||
2. ? Implemented `EnvelopeAuthService` with `HttpContext` access
|
|
||||||
3. ? Registered service in `Program.cs`
|
|
||||||
4. ?? **REVERTED** `EnvelopeReceiverPage.razor` changes (merge conflict)
|
|
||||||
|
|
||||||
### ? TODO (Next Agent)
|
|
||||||
|
|
||||||
#### 1. Re-apply EnvelopeReceiverPage.razor Changes
|
|
||||||
**File:** `EnvelopeGenerator.Server/EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor`
|
|
||||||
|
|
||||||
**Steps:**
|
|
||||||
1. Add using statement:
|
|
||||||
```razor
|
|
||||||
@using EnvelopeGenerator.Server.Services
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Replace injection:
|
|
||||||
```razor
|
|
||||||
@inject IEnvelopeAuthService EnvelopeAuth
|
|
||||||
@inject IHttpClientFactory HttpClientFactory
|
|
||||||
```
|
|
||||||
|
|
||||||
Remove:
|
|
||||||
```razor
|
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Update `OnInitializedAsync()` authentication check:
|
|
||||||
```csharp
|
|
||||||
// Replace this:
|
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
|
||||||
if (!hasAccess) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// With this:
|
|
||||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Update `LogoutAsync()` method:
|
|
||||||
```csharp
|
|
||||||
async Task LogoutAsync() {
|
|
||||||
if (string.IsNullOrWhiteSpace(EnvelopeKey) || _isLoggingOut) return;
|
|
||||||
_isLoggingOut = true;
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// ? SSR: Direct HTTP call instead of WASM client service
|
|
||||||
using var http = HttpClientFactory.CreateClient("EnvelopeGenerator.Server");
|
|
||||||
await http.PostAsync($"/api/auth/logout/envelope/{Uri.EscapeDataString(EnvelopeKey)}", null);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.LogError(ex, "Logout failed for envelope {EnvelopeKey}", EnvelopeKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Test Authentication Flow
|
|
||||||
**Scenarios:**
|
|
||||||
- ? Valid cookie ? Page loads
|
|
||||||
- ? Invalid cookie ? Redirect to login
|
|
||||||
- ? No cookie ? Redirect to login
|
|
||||||
- ? Envelope key mismatch ? Redirect to login
|
|
||||||
- ? Logout ? Cookie cleared, redirect to login
|
|
||||||
|
|
||||||
#### 3. Remove WASM Client Services from SSR Pages
|
|
||||||
**Optional Cleanup:**
|
|
||||||
- Review other SSR pages (`EnvelopeReceiverPage_DxPdfViewer.razor`, etc.)
|
|
||||||
- Replace WASM client services with SSR equivalents where applicable
|
|
||||||
- Document which services are WASM-only vs SSR-compatible
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Authentication Flow Comparison
|
|
||||||
|
|
||||||
### ? Old Flow (WASM Client Service in SSR)
|
|
||||||
```
|
|
||||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
|
||||||
?
|
|
||||||
AuthService.CheckEnvelopeAccessAsync() (WASM client)
|
|
||||||
?
|
|
||||||
IHttpClientFactory.CreateClient("EnvelopeGenerator.Server")
|
|
||||||
?
|
|
||||||
GET /api/auth/check/envelope/{key}
|
|
||||||
?
|
|
||||||
[SELF-REFERENCING REQUEST - FAILS]
|
|
||||||
?
|
|
||||||
Returns false even when authenticated
|
|
||||||
```
|
|
||||||
|
|
||||||
### ? New Flow (SSR Service)
|
|
||||||
```
|
|
||||||
EnvelopeReceiverPage (@rendermode InteractiveServer)
|
|
||||||
?
|
|
||||||
IEnvelopeAuthService.IsAuthenticated(envelopeKey)
|
|
||||||
?
|
|
||||||
IHttpContextAccessor.HttpContext.User (Direct access)
|
|
||||||
?
|
|
||||||
ClaimsPrincipal.FindFirst("sub" or NameIdentifier)
|
|
||||||
?
|
|
||||||
Compare with envelopeKey
|
|
||||||
?
|
|
||||||
Return true/false (synchronous, no HTTP)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Technical Decisions
|
|
||||||
|
|
||||||
### Why Not Use `[Authorize]` Attribute?
|
|
||||||
- Blazor SSR components **don't support** `[Authorize]` at component level
|
|
||||||
- Would require `<AuthorizeView>` component (less clean)
|
|
||||||
- Custom service provides more control + logging
|
|
||||||
|
|
||||||
### Why Scoped Lifetime?
|
|
||||||
- `IHttpContextAccessor` is scoped (per-request)
|
|
||||||
- `EnvelopeAuthService` depends on `IHttpContextAccessor`
|
|
||||||
- Scoped ensures same `HttpContext` throughout request
|
|
||||||
|
|
||||||
### Why Two Claims (`NameIdentifier` + `"sub"`)?
|
|
||||||
- **`NameIdentifier`**: Standard .NET claim type
|
|
||||||
- **`"sub"`**: JWT standard claim
|
|
||||||
- Fallback ensures compatibility with different token formats
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Logging & Debugging
|
|
||||||
|
|
||||||
### Log Levels
|
|
||||||
- **Debug:** Successful authentication
|
|
||||||
- **Warning:** Null envelope key, key mismatch
|
|
||||||
- **Error:** (Reserved for future exceptions)
|
|
||||||
|
|
||||||
### Sample Logs
|
|
||||||
```
|
|
||||||
[Debug] User authenticated for envelope 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
|
||||||
[Warning] Envelope key mismatch: Expected abc123, Got 517bb9c5-6082-4e61-aaa5-9846386e67ee
|
|
||||||
[Warning] IsAuthenticated called with null or empty envelope key
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Testing Checklist
|
|
||||||
|
|
||||||
### Unit Tests (TODO)
|
|
||||||
```csharp
|
|
||||||
// EnvelopeGenerator.Tests/Services/EnvelopeAuthServiceTests.cs
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_ValidUser_ReturnsTrue() { ... }
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_InvalidKey_ReturnsFalse() { ... }
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IsAuthenticated_UnauthenticatedUser_ReturnsFalse() { ... }
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GetAuthenticatedEnvelopeKey_ValidUser_ReturnsKey() { ... }
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests (Manual)
|
|
||||||
1. ? Login with valid access code ? Cookie set
|
|
||||||
2. ? Navigate to `/envelope/{key}` ? Page loads
|
|
||||||
3. ? Logout ? Cookie cleared, redirect
|
|
||||||
4. ? Try accessing `/envelope/{key}` without cookie ? Redirect to login
|
|
||||||
5. ? Try accessing `/envelope/{wrongKey}` with valid cookie ? Redirect to login
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Migration Checklist
|
|
||||||
|
|
||||||
- [x] Create `IEnvelopeAuthService` interface
|
|
||||||
- [x] Implement `EnvelopeAuthService`
|
|
||||||
- [x] Register service in `Program.cs`
|
|
||||||
- [ ] **Re-apply** `EnvelopeReceiverPage.razor` changes (after merge)
|
|
||||||
- [ ] Test authentication flow
|
|
||||||
- [ ] Add unit tests
|
|
||||||
- [ ] Update other SSR pages (if needed)
|
|
||||||
- [ ] Document in `COPILOT_CONTEXT.md`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Documentation Updates Needed
|
|
||||||
|
|
||||||
### COPILOT_CONTEXT.md
|
|
||||||
|
|
||||||
**Add Section:**
|
|
||||||
```markdown
|
|
||||||
## SSR Authentication Service
|
|
||||||
|
|
||||||
**Purpose:** Server-side authentication for Blazor InteractiveServer pages.
|
|
||||||
|
|
||||||
**Location:** `EnvelopeGenerator.Server/Services/`
|
|
||||||
|
|
||||||
**Service:** `IEnvelopeAuthService` / `EnvelopeAuthService`
|
|
||||||
|
|
||||||
**Usage:**
|
|
||||||
```razor
|
|
||||||
@inject IEnvelopeAuthService EnvelopeAuth
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync() {
|
|
||||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why Not Use WASM Client Services in SSR?**
|
|
||||||
- WASM client services use `IHttpClientFactory` with base address configuration
|
|
||||||
- SSR context requires `HttpContext` to configure base address
|
|
||||||
- Calling API endpoints from server-side component creates self-referencing requests
|
|
||||||
- Use `IEnvelopeAuthService` for direct `HttpContext.User` access instead
|
|
||||||
|
|
||||||
**Authentication Flow:**
|
|
||||||
1. JWT token stored in per-envelope cookie (`AuthTokenSignFLOWReceiver.{envelopeKey}`)
|
|
||||||
2. JWT middleware validates token, sets `HttpContext.User`
|
|
||||||
3. `EnvelopeAuthService` checks `ClaimsPrincipal.FindFirst("sub")` or `NameIdentifier`
|
|
||||||
4. Compares claim value with route parameter `{EnvelopeKey}`
|
|
||||||
|
|
||||||
**Claim Priority:**
|
|
||||||
1. `ClaimTypes.NameIdentifier` (standard .NET)
|
|
||||||
2. `"sub"` (JWT standard)
|
|
||||||
|
|
||||||
**Service Lifetime:** Scoped (per-request)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Common Mistakes to Avoid
|
|
||||||
|
|
||||||
### ? Don't Do This
|
|
||||||
```csharp
|
|
||||||
// SSR page using WASM client service
|
|
||||||
@inject EnvelopeGenerator.Server.Client.Services.AuthService AuthService
|
|
||||||
|
|
||||||
var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey);
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why Wrong:**
|
|
||||||
- Creates self-referencing HTTP request
|
|
||||||
- WASM client service doesn't work in SSR context
|
|
||||||
- Always returns `false` even when authenticated
|
|
||||||
|
|
||||||
### ? Do This Instead
|
|
||||||
```csharp
|
|
||||||
// SSR page using SSR authentication service
|
|
||||||
@inject IEnvelopeAuthService EnvelopeAuth
|
|
||||||
|
|
||||||
if (!EnvelopeAuth.IsAuthenticated(EnvelopeKey)) {
|
|
||||||
Navigation.NavigateTo($"/envelope/login/{Uri.EscapeDataString(EnvelopeKey)}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why Correct:**
|
|
||||||
- Direct `HttpContext.User` access
|
|
||||||
- Synchronous (no HTTP overhead)
|
|
||||||
- Works in SSR context
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
### Related Files
|
|
||||||
- `EnvelopeGenerator.Server/Program.cs` (Service registration, JWT middleware)
|
|
||||||
- `EnvelopeGenerator.Server.Client/Services/AuthService.cs` (WASM client version)
|
|
||||||
- `EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor` (WASM login page)
|
|
||||||
- `EnvelopeGenerator.Server/Components/Pages/EnvelopeReceiverPage.razor` (SSR viewer page)
|
|
||||||
|
|
||||||
### JWT Configuration
|
|
||||||
**File:** `EnvelopeGenerator.Server/Program.cs`
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
.AddJwtBearer(AuthScheme.Receiver, opt =>
|
|
||||||
{
|
|
||||||
opt.Events = new JwtBearerEvents
|
|
||||||
{
|
|
||||||
OnMessageReceived = context =>
|
|
||||||
{
|
|
||||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
|
||||||
if (envelopeKey is not null)
|
|
||||||
{
|
|
||||||
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
|
|
||||||
if (context.Request.Cookies.TryGetValue(cookieName, out var cookieToken))
|
|
||||||
context.Token = cookieToken;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
},
|
|
||||||
OnTokenValidated = context =>
|
|
||||||
{
|
|
||||||
var envelopeKey = context.Request.Path.Value?.Split('/').LastOrDefault();
|
|
||||||
var sub = context.Principal?.FindFirst("sub")?.Value;
|
|
||||||
|
|
||||||
if (envelopeKey is null || sub != envelopeKey)
|
|
||||||
context.Fail("Envelope key mismatch");
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**What Was Done:**
|
|
||||||
1. Created SSR authentication service (`IEnvelopeAuthService` / `EnvelopeAuthService`)
|
|
||||||
2. Registered service in DI container
|
|
||||||
3. Updated `EnvelopeReceiverPage.razor` (REVERTED due to merge)
|
|
||||||
|
|
||||||
**What's Left:**
|
|
||||||
1. **Re-apply** `EnvelopeReceiverPage.razor` changes after merge
|
|
||||||
2. Test authentication flow
|
|
||||||
3. Add unit tests
|
|
||||||
4. Update documentation
|
|
||||||
|
|
||||||
**Key Insight:**
|
|
||||||
- **WASM client services ? SSR server services**
|
|
||||||
- Use `IHttpContextAccessor` for direct `HttpContext.User` access in SSR
|
|
||||||
- Avoid HTTP requests from server-side components to own endpoints
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Last Updated:** 2025-01-27
|
|
||||||
**Status:** ?? Partial (Service created, page changes reverted for merge)
|
|
||||||
**Next Agent:** Re-apply `EnvelopeReceiverPage.razor` changes + testing
|
|
||||||
1272
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
1272
RECEIVER_PDF_VIEWER_CONTEXT.md
Normal file
File diff suppressed because it is too large
Load Diff
290
SESSION_SUMMARY.md
Normal file
290
SESSION_SUMMARY.md
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
# Session Summary - DevExpress Migration Research
|
||||||
|
**Date:** June 30, 2026
|
||||||
|
**Task:** Research and plan PDF.js to DevExpress DxPdfViewer migration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What We Accomplished
|
||||||
|
|
||||||
|
### 1. Documentation Review & Translation ✅
|
||||||
|
- Translated all Turkish sections in `DEBUG_NOTES.md` to English
|
||||||
|
- Translated all Turkish sections in `DEVEXPRESS_V25_LIMITATIONS.md` to English
|
||||||
|
- Translated all Turkish sections in `TESTING_CHECKLIST.md` to English
|
||||||
|
- Fixed character encoding issues (? symbols → proper characters)
|
||||||
|
|
||||||
|
### 2. API Verification ✅
|
||||||
|
- Verified DevExpress DxPdfViewer v25.2.3 API from official documentation
|
||||||
|
- **Discovered:** `ZoomLevelChanged` event IS available (contrary to initial documentation)
|
||||||
|
- **Confirmed:** No `ActivePageIndexChanged` or `PageNumberChanged` events
|
||||||
|
- **Confirmed:** `ActivePageIndex` is read-only (no programmatic page navigation)
|
||||||
|
- **Confirmed:** No `GoToPageAsync()` or similar methods
|
||||||
|
|
||||||
|
### 3. Complete API Documentation ✅
|
||||||
|
|
||||||
|
**Available in v25.2.3:**
|
||||||
|
- `DocumentContent` (byte[]) - Two-way bindable
|
||||||
|
- `ZoomLevel` (double) - Two-way bindable, factor not percentage
|
||||||
|
- `ActivePageIndex` (int) - Read-only, 0-based
|
||||||
|
- `PageCount` (int) - Read-only
|
||||||
|
- `CustomizeToolbar` event - Toolbar customization
|
||||||
|
- `ZoomLevelChanged` event - EventCallback<double>
|
||||||
|
- `PrintAsync()` method
|
||||||
|
- `DownloadAsync()` method
|
||||||
|
|
||||||
|
**NOT Available:**
|
||||||
|
- ❌ No page navigation API (GoToPageAsync, etc.)
|
||||||
|
- ❌ No ActivePageIndexChanged event
|
||||||
|
- ❌ ActivePageIndex is read-only (cannot set)
|
||||||
|
|
||||||
|
### 4. Migration Strategy Defined ✅
|
||||||
|
|
||||||
|
**Chosen Approach:** Hybrid Implementation
|
||||||
|
- DevExpress DxPdfViewer for PDF rendering
|
||||||
|
- Custom toolbar via `CustomizeToolbar` event
|
||||||
|
- Manual state tracking for current page/zoom
|
||||||
|
- `ZoomLevelChanged` event for zoom synchronization
|
||||||
|
- JavaScript overlays for signature buttons (keep existing)
|
||||||
|
- Graceful degradation for unavailable features
|
||||||
|
|
||||||
|
**What Works:**
|
||||||
|
- ✅ PDF rendering with DevExpress
|
||||||
|
- ✅ Custom zoom controls via toolbar
|
||||||
|
- ✅ Zoom level synchronization via event
|
||||||
|
- ✅ Signature overlays (JavaScript, unchanged)
|
||||||
|
- ✅ Signature capture workflow (unchanged)
|
||||||
|
|
||||||
|
**Known Limitations:**
|
||||||
|
- ⚠️ User scrolling in viewer doesn't trigger C# page tracking
|
||||||
|
- ⚠️ Thumbnail clicks can't navigate viewer (only state updates)
|
||||||
|
- ⚠️ Signature button clicks can't navigate viewer to that page
|
||||||
|
- ⚠️ Must use custom toolbar for navigation
|
||||||
|
|
||||||
|
### 5. Complete Implementation Plan Created ✅
|
||||||
|
|
||||||
|
**Created:** `MIGRATION_PLAN.md` with:
|
||||||
|
- Step-by-step implementation guide (10 steps)
|
||||||
|
- Complete code examples for each step
|
||||||
|
- Testing checklist (30+ items)
|
||||||
|
- Success criteria
|
||||||
|
- Rollback plan
|
||||||
|
- Timeline estimate: 7-8 hours
|
||||||
|
|
||||||
|
### 6. Updated Documentation ✅
|
||||||
|
|
||||||
|
**Updated:** `DEVEXPRESS_V25_LIMITATIONS.md`
|
||||||
|
- Removed `ZoomLevelChanged` from "Missing Events" section
|
||||||
|
- Added to "Available Events" section
|
||||||
|
- Corrected notes about AI-suggested APIs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Decisions Made
|
||||||
|
|
||||||
|
### Decision 1: Hybrid Approach vs Multi-Instance
|
||||||
|
**Chosen:** Hybrid (single DxPdfViewer + custom toolbar)
|
||||||
|
**Rejected:** Multi-instance (separate viewer per page)
|
||||||
|
**Reason:** Memory/performance concerns for 30+ page PDFs
|
||||||
|
|
||||||
|
### Decision 2: CustomizeToolbar for Navigation
|
||||||
|
**Chosen:** Custom toolbar buttons for page navigation
|
||||||
|
**Alternative:** Extract single page to byte[] and swap DocumentContent
|
||||||
|
**Reason:** More maintainable, better UX, proven pattern
|
||||||
|
|
||||||
|
### Decision 3: Keep JavaScript Overlays
|
||||||
|
**Chosen:** Continue using PDF.js overlay JavaScript
|
||||||
|
**Alternative:** Pure Blazor overlays with absolute positioning
|
||||||
|
**Reason:** Already working, coordinate system already solved, no rework needed
|
||||||
|
|
||||||
|
### Decision 4: ZoomLevelChanged Event Utilization
|
||||||
|
**Chosen:** Use `ZoomLevelChanged` to synchronize overlays
|
||||||
|
**Alternative:** Manual zoom tracking only
|
||||||
|
**Reason:** Native zoom controls in DevExpress toolbar will trigger event, keeping overlays in sync
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
### Created:
|
||||||
|
1. `MIGRATION_PLAN.md` - Complete step-by-step migration guide
|
||||||
|
2. `SESSION_SUMMARY.md` - This file
|
||||||
|
|
||||||
|
### Modified:
|
||||||
|
1. `DEBUG_NOTES.md` - Translated Turkish → English
|
||||||
|
2. `DEVEXPRESS_V25_LIMITATIONS.md` - Translated + corrected ZoomLevelChanged availability
|
||||||
|
3. `TESTING_CHECKLIST.md` - Translated Turkish → English
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Findings
|
||||||
|
|
||||||
|
### Finding 1: ZoomLevelChanged Event Available
|
||||||
|
**Impact:** Medium-High
|
||||||
|
**Details:** Initial documentation incorrectly stated this event was missing. It IS available in v25.2.3, which improves zoom synchronization capabilities.
|
||||||
|
|
||||||
|
**Benefit:** Can detect when user uses native DevExpress zoom controls and update signature overlays accordingly.
|
||||||
|
|
||||||
|
### Finding 2: No Page Navigation API
|
||||||
|
**Impact:** High
|
||||||
|
**Details:** Absolutely no way to programmatically navigate viewer to specific page. `ActivePageIndex` is read-only, no methods available.
|
||||||
|
|
||||||
|
**Workaround:** Custom toolbar buttons that update manual state tracking. User must use these buttons instead of scrolling or native viewer navigation.
|
||||||
|
|
||||||
|
### Finding 3: No Page Change Event
|
||||||
|
**Impact:** Medium
|
||||||
|
**Details:** When user scrolls in native viewer, no C# event fires. Cannot detect page changes.
|
||||||
|
|
||||||
|
**Workaround:** Disable native scrolling via `IsSinglePagePreview="true"`, force use of custom toolbar buttons only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Remaining Work
|
||||||
|
|
||||||
|
### Immediate Next Steps:
|
||||||
|
1. Create feature branch `feature/devexpress-migration`
|
||||||
|
2. Backup current `EnvelopeReceiverPage.razor`
|
||||||
|
3. Begin implementation following `MIGRATION_PLAN.md` steps 1-10
|
||||||
|
|
||||||
|
### Testing Required:
|
||||||
|
1. Basic PDF rendering (Phase 1)
|
||||||
|
2. Navigation with custom toolbar (Phase 2)
|
||||||
|
3. Zoom synchronization (Phase 3)
|
||||||
|
4. Signature overlays (Phase 4)
|
||||||
|
5. Full signature workflow (Phase 5)
|
||||||
|
6. Edge cases and limitations (Phase 6)
|
||||||
|
|
||||||
|
### Documentation Required:
|
||||||
|
1. User-facing documentation about navigation limitations
|
||||||
|
2. Update `RECEIVER_PDF_VIEWER_CONTEXT.md` with DevExpress details
|
||||||
|
3. Add tooltips/help text in UI explaining custom toolbar usage
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
### Low Risk ✅
|
||||||
|
- PDF rendering - DevExpress is proven
|
||||||
|
- Signature capture - No changes to existing modal
|
||||||
|
- Zoom controls - ZoomLevelChanged event available
|
||||||
|
- Signature overlays - Keep existing JavaScript
|
||||||
|
|
||||||
|
### Medium Risk ⚠️
|
||||||
|
- Custom toolbar UX - Users must learn new navigation pattern
|
||||||
|
- Page tracking - Manual state only, can desync if user finds way to scroll
|
||||||
|
- Performance - DevExpress rendering speed vs PDF.js unknown
|
||||||
|
|
||||||
|
### High Risk ❌
|
||||||
|
- None identified - Rollback plan exists if critical issues found
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
|
||||||
|
**Migration is successful if:**
|
||||||
|
1. All signature workflows complete successfully
|
||||||
|
2. PDF rendering performance is acceptable (<2s for 20-page PDF)
|
||||||
|
3. No critical bugs in signature placement
|
||||||
|
4. Custom toolbar navigation works reliably
|
||||||
|
5. Mobile/tablet layout works
|
||||||
|
6. Users can complete signing workflow without confusion
|
||||||
|
|
||||||
|
**Migration is failed if:**
|
||||||
|
1. Signature positioning is broken
|
||||||
|
2. Performance is significantly worse than PDF.js
|
||||||
|
3. Critical workflow steps are blocked
|
||||||
|
4. Mobile layout is unusable
|
||||||
|
|
||||||
|
→ If failed, rollback to PDF.js implementation on master branch
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Questions Answered
|
||||||
|
|
||||||
|
### Q1: Can we use DevExpress DxPdfViewer in v25.2.3?
|
||||||
|
**A:** Yes, but with significant API limitations requiring workarounds.
|
||||||
|
|
||||||
|
### Q2: Can we programmatically navigate to specific pages?
|
||||||
|
**A:** No. Must use custom toolbar buttons with manual state tracking.
|
||||||
|
|
||||||
|
### Q3: Can we detect when user changes pages or zoom?
|
||||||
|
**A:** Zoom yes (`ZoomLevelChanged` event), page changes no.
|
||||||
|
|
||||||
|
### Q4: Do we need to rewrite signature overlay logic?
|
||||||
|
**A:** No. Keep existing PDF.js JavaScript for overlays.
|
||||||
|
|
||||||
|
### Q5: How long will migration take?
|
||||||
|
**A:** Estimated 7-8 hours implementation + testing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations
|
||||||
|
|
||||||
|
### For Immediate Action:
|
||||||
|
1. ✅ Proceed with migration using hybrid approach
|
||||||
|
2. ✅ Use `MIGRATION_PLAN.md` as implementation guide
|
||||||
|
3. ✅ Create feature branch before starting
|
||||||
|
4. ✅ Test thoroughly with multi-page PDFs and multiple signatures
|
||||||
|
|
||||||
|
### For Future Consideration:
|
||||||
|
1. Monitor DevExpress v26.x for API improvements (page navigation, events)
|
||||||
|
2. Consider user feedback on custom toolbar navigation
|
||||||
|
3. Evaluate if `IsSinglePagePreview="true"` provides best UX
|
||||||
|
4. Consider adding help tooltips for navigation buttons
|
||||||
|
|
||||||
|
### Not Recommended:
|
||||||
|
1. ❌ Multi-instance approach (memory concerns)
|
||||||
|
2. ❌ Full rewrite of signature overlay logic (unnecessary)
|
||||||
|
3. ❌ Trying to hack `ActivePageIndex` with reflection (breaks on updates)
|
||||||
|
4. ❌ JavaScript interop to control DevExpress viewer (unsupported)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Debt Notes
|
||||||
|
|
||||||
|
### Introduced by Migration:
|
||||||
|
1. Manual page state tracking (desync possible if native scrolling enabled)
|
||||||
|
2. Custom toolbar implementation (must maintain alongside DevExpress updates)
|
||||||
|
3. Mixed Blazor/JavaScript architecture (overlays in JS, viewer in Blazor)
|
||||||
|
|
||||||
|
### Mitigated by Migration:
|
||||||
|
1. PDF.js version management (DevExpress handles PDF rendering)
|
||||||
|
2. PDF.js security updates (DevExpress responsibility)
|
||||||
|
3. Cross-browser PDF rendering quirks (DevExpress tested)
|
||||||
|
|
||||||
|
### Neutral:
|
||||||
|
1. Signature capture logic (unchanged)
|
||||||
|
2. Coordinate system complexity (unchanged, still INCHES in DB)
|
||||||
|
3. Redis/SQL signature caching (unchanged)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
**Research Phase: COMPLETE ✅**
|
||||||
|
|
||||||
|
We have thoroughly researched the DevExpress DxPdfViewer v25.2.3 API, identified all limitations, designed a viable hybrid migration strategy, and created a complete implementation plan.
|
||||||
|
|
||||||
|
**Key Insight:** The migration is feasible with acceptable tradeoffs. The main limitation (no programmatic page navigation) can be worked around with custom toolbar buttons. The availability of `ZoomLevelChanged` event is a positive discovery that improves the solution.
|
||||||
|
|
||||||
|
**Recommendation:** Proceed with migration following `MIGRATION_PLAN.md`.
|
||||||
|
|
||||||
|
**Next Session:** Begin implementation starting with Steps 1-2 (component structure).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Session Statistics
|
||||||
|
|
||||||
|
- Documentation pages reviewed: 5+ DevExpress documentation pages
|
||||||
|
- Files translated: 3 (DEBUG_NOTES.md, DEVEXPRESS_V25_LIMITATIONS.md, TESTING_CHECKLIST.md)
|
||||||
|
- Files created: 2 (MIGRATION_PLAN.md, SESSION_SUMMARY.md)
|
||||||
|
- Files updated: 3
|
||||||
|
- API endpoints verified: 15+
|
||||||
|
- Code examples written: 10+
|
||||||
|
- Test cases defined: 30+
|
||||||
|
- Estimated implementation time: 7-8 hours
|
||||||
|
- Estimated testing time: 2 hours
|
||||||
|
|
||||||
|
**Total Documentation:** ~500 lines of comprehensive migration guidance
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**End of Session Summary**
|
||||||
161
TESTING_CHECKLIST.md
Normal file
161
TESTING_CHECKLIST.md
Normal file
@@ -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`
|
||||||
Reference in New Issue
Block a user