From 3b4278d7e02fba9e511e56fcf456662ccfda0f75 Mon Sep 17 00:00:00 2001 From: TekH Date: Thu, 11 Jun 2026 11:52:11 +0200 Subject: [PATCH] Add envelope viewer with signature functionality Introduced a new Razor page `EnvelopeReceiverPage_DxReportViewer.razor` to manage and sign envelope-related documents. Integrated DevExpress Blazor components for PDF rendering and signature handling. Key features: - Added sections for envelope info, signature actions, and a PDF viewer. - Enabled signature creation via drawing, text input, or image upload. - Validated and applied signatures to annotated fields in the document. - Integrated JavaScript interop for signature capture and rendering. - Supported exporting signed PDFs and dynamic signature overlays. - Ensured proper resource cleanup with `IDisposable` implementation. --- .../EnvelopeReceiverPage_DxReportViewer.razor | 734 ++++++++++++++++++ 1 file changed, 734 insertions(+) create mode 100644 EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage_DxReportViewer.razor diff --git a/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage_DxReportViewer.razor b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage_DxReportViewer.razor new file mode 100644 index 00000000..3d978f2b --- /dev/null +++ b/EnvelopeGenerator.ReceiverUI/Pages/EnvelopeReceiverPage_DxReportViewer.razor @@ -0,0 +1,734 @@ +@page "/envelope/{EnvelopeKey}/DxReportViewer" +@using System.Drawing +@using DevExpress.Blazor +@using DevExpress.Drawing +@using DevExpress.Utils +@using DevExpress.XtraPrinting +@using DevExpress.XtraPrinting.Drawing +@using Microsoft.JSInterop +@using XtraReport = DevExpress.XtraReports.UI.XtraReport +@using BottomMarginBand = DevExpress.XtraReports.UI.BottomMarginBand +@using XRLabel = DevExpress.XtraReports.UI.XRLabel +@using XRPictureBox = DevExpress.XtraReports.UI.XRPictureBox +@using XRControl = DevExpress.XtraReports.UI.XRControl +@using ImageSizeMode = DevExpress.XtraPrinting.ImageSizeMode +@using EnvelopeGenerator.ReceiverUI.Services +@using DevExpress.Blazor.Reporting +@using Microsoft.Extensions.Options +@using EnvelopeGenerator.ReceiverUI.Options +@using EnvelopeGenerator.ReceiverUI.Models +@implements IDisposable +@inject IJSRuntime JSRuntime +@inject AnnotationService AnnotationService +@inject IOptions AppOptions +@inject NavigationManager Navigation +@inject InMemoryReportStorageWebExtension ReportStorage +@inject EnvelopeGenerator.ReceiverUI.Services.DocumentService DocumentService +@inject EnvelopeGenerator.ReceiverUI.Services.AuthService AuthService +@inject EnvelopeGenerator.ReceiverUI.Services.EnvelopeReceiverService EnvelopeReceiverService + + + + +
+ +
+ +@* ?? Envelope info header ???????????????????????????????????????????????? *@ +@if (_envelopeReceiver is not null) { +
+
+
+ + + +
+
@(_envelopeReceiver.Envelope?.Title ?? "Dokument")
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName) || !string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) { +
+ Von + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.FullName)) { + @_envelopeReceiver.Envelope!.User!.FullName + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.User?.Email)) { + <@_envelopeReceiver.Envelope!.User!.Email> + } +  ·  @_envelopeReceiver.Envelope?.AddedWhen.ToString("dd.MM.yyyy") +
+ } +
+
+
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Name)) { + + + + + @_envelopeReceiver.Name + + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.CompanyName)) { + + + + + @_envelopeReceiver.CompanyName + + } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.JobTitle)) { + @_envelopeReceiver.JobTitle + } + @{ + var docElements = _envelopeReceiver.Envelope?.Documents?.FirstOrDefault()?.Elements; + int sigCount = docElements?.Count() ?? _annotations.Count; + } + @if (sigCount > 0) { + + + + + @sigCount @(sigCount == 1 ? "Unterschriftsfeld" : "Unterschriftsfelder") + + } +
+
+ @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.Envelope?.Message)) { +
@_envelopeReceiver.Envelope!.Message
+ } + @if (!string.IsNullOrWhiteSpace(_envelopeReceiver.PrivateMessage)) { +
+ + + + @_envelopeReceiver.PrivateMessage +
+ } +
+} + +@* ?? Signature action bar ???????????????????????????????????????????????? *@ +
+
+ + + @if (_annotations.Count > 0 && !SignatureApplied) { +
+
+
+
+ + @_checkedAnnotations.Count / @_annotations.Count + Seite@(AnnotationPages.Count() == 1 ? "" : "n") @string.Join(",", AnnotationPages) + + @if (_capturedSignature is null) { + Zuerst Unterschrift erstellen + } else if (_checkedAnnotations.Count == _annotations.Count) { + ✓ Wird angewendet… + } +
+ } + + @if (!string.IsNullOrWhiteSpace(SignatureValidationMessage)) { + @SignatureValidationMessage + } + @if (SignatureApplied) { + + + + + Unterschrift angewendet + + } + +
+ + @if (!string.IsNullOrWhiteSpace(EnvelopeKey)) { + + } +
+
+
+ +
+ + + + + + @if(ActiveSignatureTab == SignatureTabDraw) { +

Bitte unterschreiben Sie im folgenden Feld.

+ + } else if(ActiveSignatureTab == SignatureTabText) { +

Geben Sie Ihre Unterschrift als Text ein und waehlen Sie eine Schriftart.

+
+
+ +
+
+ +
+
+ + } else { +

Laden Sie ein Bild Ihrer Unterschrift hoch.

+ + + } + + +
+

Bitte geben Sie die folgenden Angaben ein. Das Datum wird automatisch hinzugefuegt.

+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + @if(!string.IsNullOrWhiteSpace(PopupValidationMessage)) { +
@PopupValidationMessage
+ } +
+ +
+ + +
+
+
+ +
+@if(Report is not null) { + +} +
+ +
+ +@code { + + const string SignatureTabDraw = "draw"; + const string SignatureTabText = "text"; + const string SignatureTabImage = "image"; + const string DrawCanvasId = "receiver-signature-pad"; + const string TypedCanvasId = "receiver-typed-signature-pad"; + const string ImageInputId = "receiver-signature-image-input"; + const string ImageCanvasId = "receiver-image-signature-pad"; + + readonly (string Text, string Value)[] TypedSignatureFonts = { + ("Brush Script", "'Brush Script MT', cursive"), + ("Segoe Script", "'Segoe Script', cursive"), + ("Lucida Handwriting", "'Lucida Handwriting', cursive"), + ("Comic Sans", "'Comic Sans MS', cursive"), + ("Cursive", "cursive") +}; + + [Parameter] public string EnvelopeKey { get; set; } = string.Empty; + + DxReportViewer? reportViewer; + XtraReport? Report; + bool SignatureApplied; + bool SignaturePopupVisible; + string? SignatureValidationMessage; + string? PopupValidationMessage; + string ActiveSignatureTab = SignatureTabDraw; + string TypedSignatureText = string.Empty; + string TypedSignatureFont = "'Brush Script MT', cursive"; + string SignerFullName = string.Empty; + string SignerPosition = string.Empty; + string SignaturePlace = string.Empty; + int ViewerKey; + bool IsLoggingOut; + + IReadOnlyList _annotations = []; + IEnumerable AnnotationPages => _annotations.Select(a => a.Page).Distinct().OrderBy(p => p); + EnvelopeReceiverDto? _envelopeReceiver; + record SignatureCapture(string DataUrl, string FullName, string Position, string Place); + SignatureCapture? _capturedSignature; + byte[]? _basePdfBytes; + // annotation IDs the user has checked via overlay checkboxes + readonly HashSet _checkedAnnotations = []; + DotNetObjectReference? _dotNetRef; + int _lastOverlayViewerKey = -1; + + async Task LogoutAsync() { + if (string.IsNullOrWhiteSpace(EnvelopeKey) || IsLoggingOut) return; + IsLoggingOut = true; + await InvokeAsync(StateHasChanged); + await AuthService.LogoutEnvelopeReceiverAsync(EnvelopeKey); + Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: true); + } + + protected override async Task OnInitializedAsync() { + + // ? REDIRECT: /receiver/{key} -> /envelope/{key} (NEW PDF.js viewer) + if (!string.IsNullOrWhiteSpace(EnvelopeKey)) { + Navigation.NavigateTo($"/envelope/{Uri.EscapeDataString(EnvelopeKey)}", forceLoad: false); + return; + } + + if (!string.IsNullOrWhiteSpace(EnvelopeKey)) { + var hasAccess = await AuthService.CheckEnvelopeAccessAsync(EnvelopeKey); + if (!hasAccess) { + Navigation.NavigateTo($"/login/{Uri.EscapeDataString(EnvelopeKey)}"); + return; + } + else + { + ActiveSignatureTab = SignatureTabDraw; + SignaturePopupVisible = true; + SignatureValidationMessage = null; + PopupValidationMessage = null; + } + } + + _annotations = await AnnotationService.GetAnnotationsAsync(EnvelopeKey ?? "fake"); + _envelopeReceiver = await EnvelopeReceiverService.GetAsync(EnvelopeKey ?? "fake"); + + if (!AppOptions.Value.ForceToUseFakeDocument && !string.IsNullOrWhiteSpace(EnvelopeKey)) { + var (pdfBytes, _) = await DocumentService.GetDocumentAsync(EnvelopeKey); + if (pdfBytes is { Length: > 0 }) + _basePdfBytes = pdfBytes; + } + + var initialReport = BuildFreshBaseReport(); + Report = initialReport; + } + + protected override async Task OnAfterRenderAsync(bool firstRender) { + if (firstRender) + _dotNetRef = DotNetObjectReference.Create(this); + + + if (Report is not null && _annotations.Count > 0 + && _capturedSignature is not null && !SignatureApplied + && _lastOverlayViewerKey != ViewerKey) { + _lastOverlayViewerKey = ViewerKey; + await JSRuntime.InvokeVoidAsync( + "receiverSignature.installAnnotationCheckboxes", + _annotations, _checkedAnnotations.ToArray(), _dotNetRef); + } + } + + [JSInvokable] + public async Task OnAnnotationToggled(long annotationId, bool isChecked) { + if (isChecked) + _checkedAnnotations.Add(annotationId); + else + _checkedAnnotations.Remove(annotationId); + await InvokeAsync(StateHasChanged); + + if (_capturedSignature is not null + && !SignatureApplied + && _annotations.Count > 0 + && _checkedAnnotations.Count == _annotations.Count) { + // K?sa bekleme: kullan?c? son tick'in görsel feedback'ini görsün + await Task.Delay(400); + await SubmitSignaturesAsync(); + } + } + + void OpenSignaturePopupAsync() { + ActiveSignatureTab = SignatureTabDraw; + SignaturePopupVisible = true; + SignatureValidationMessage = null; + PopupValidationMessage = null; + } + + async Task OnPopupShownAsync() { + await InitializeActiveSignatureTabAsync(); + } + + async Task SetSignatureTabAsync(string tab) { + ActiveSignatureTab = tab; + PopupValidationMessage = null; + await InvokeAsync(StateHasChanged); + await Task.Delay(50); + await InitializeActiveSignatureTabAsync(); + } + + async Task InitializeActiveSignatureTabAsync() { + if(ActiveSignatureTab == SignatureTabDraw) { + await JSRuntime.InvokeVoidAsync("receiverSignature.initialize", DrawCanvasId); + } else if(ActiveSignatureTab == SignatureTabText) { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeTyped", TypedCanvasId); + await RenderTypedSignatureAsync(); + } else { + await JSRuntime.InvokeVoidAsync("receiverSignature.initializeImage", ImageInputId, ImageCanvasId); + } + } + + async Task RenewSignatureAsync() { + PopupValidationMessage = null; + + if(ActiveSignatureTab == SignatureTabDraw) { + await JSRuntime.InvokeVoidAsync("receiverSignature.clear", DrawCanvasId); + } else if(ActiveSignatureTab == SignatureTabText) { + TypedSignatureText = string.Empty; + await JSRuntime.InvokeVoidAsync("receiverSignature.clearTyped", TypedCanvasId); + } else { + await JSRuntime.InvokeVoidAsync("receiverSignature.clearImage", ImageInputId, ImageCanvasId); + } + } + + void CloseSignaturePopup() { + PopupValidationMessage = null; + SignaturePopupVisible = false; + } + + async Task OnTypedSignatureChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { + TypedSignatureText = args.Value?.ToString() ?? string.Empty; + await RenderTypedSignatureAsync(); + } + + async Task OnTypedSignatureFontChanged(Microsoft.AspNetCore.Components.ChangeEventArgs args) { + TypedSignatureFont = args.Value?.ToString() ?? TypedSignatureFont; + await RenderTypedSignatureAsync(); + } + + async Task RenderTypedSignatureAsync() { + await JSRuntime.InvokeVoidAsync("receiverSignature.renderTypedSignature", TypedCanvasId, TypedSignatureText, TypedSignatureFont); + } + + async Task SaveSignatureAsync() { + if (string.IsNullOrWhiteSpace(SignerFullName)) { + PopupValidationMessage = "Bitte geben Sie Vor- und Nachname ein."; + return; + } + if (string.IsNullOrWhiteSpace(SignaturePlace)) { + PopupValidationMessage = "Bitte geben Sie den Ort ein."; + return; + } + var signatureDataUrl = await GetActiveSignatureDataUrlAsync(); + if (string.IsNullOrWhiteSpace(signatureDataUrl)) { + PopupValidationMessage = "Die Unterschrift ist fuer den PDF-Export erforderlich."; + return; + } + + PopupValidationMessage = null; + SignatureValidationMessage = null; + _capturedSignature = new(signatureDataUrl, SignerFullName.Trim(), SignerPosition.Trim(), SignaturePlace.Trim()); + + // If no annotations, apply immediately (no checkbox step needed) + if (_annotations.Count == 0) { + var freshReport = BuildFreshBaseReport(); + AddSignature(freshReport, _capturedSignature.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place); + Report = freshReport; + SignatureApplied = true; + SignaturePopupVisible = false; + ViewerKey++; + return; + } + + // Close popup; checkboxes will appear on the PDF via OnAfterRenderAsync + SignaturePopupVisible = false; + _lastOverlayViewerKey = -1; // force overlay reinstall + await InvokeAsync(StateHasChanged); + } + + async Task SubmitSignaturesAsync() { + if (_checkedAnnotations.Count == 0) { + SignatureValidationMessage = "Bitte markieren Sie mindestens ein Unterschriftsfeld im Dokument."; + return; + } + if (_checkedAnnotations.Count < _annotations.Count) { + SignatureValidationMessage = $"Bitte markieren Sie alle {_annotations.Count} Unterschriftsfelder. Noch {_annotations.Count - _checkedAnnotations.Count} offen."; + return; + } + + SignatureValidationMessage = null; + var freshReport = BuildFreshBaseReport(); + foreach (var ann in _annotations) + AddSignatureAtAnnotation(freshReport, ann, _capturedSignature!.DataUrl, _capturedSignature.FullName, _capturedSignature.Position, _capturedSignature.Place); + + Report = freshReport; + SignatureApplied = true; + ViewerKey++; + await InvokeAsync(StateHasChanged); + } + + async Task GetActiveSignatureDataUrlAsync() { + if(ActiveSignatureTab == SignatureTabDraw) + return await JSRuntime.InvokeAsync("receiverSignature.getDataUrl", DrawCanvasId); + + if(ActiveSignatureTab == SignatureTabText) { + await RenderTypedSignatureAsync(); + return await JSRuntime.InvokeAsync("receiverSignature.getTypedDataUrl", TypedCanvasId); + } + + return await JSRuntime.InvokeAsync("receiverSignature.getImageDataUrl", ImageCanvasId); + } + + async Task ExportSignedPdfAsync() { + if(!SignatureApplied || Report is null) { + SignatureValidationMessage = "Bitte fuegen Sie die Unterschrift zuerst zum Bericht hinzu."; + return; + } + + try { + SignatureValidationMessage = null; + await reportViewer!.ExportToAsync(ExportFormat.Pdf); + } catch(Exception) { + SignatureValidationMessage = "Das signierte PDF konnte nicht exportiert werden. Bitte laden Sie die Seite neu und versuchen Sie es erneut."; + } + } + + XtraReport BuildFreshBaseReport() { + if (_basePdfBytes is { Length: > 0 }) { + var report = new XtraReport(); + var detail = new DevExpress.XtraReports.UI.DetailBand(); + report.Bands.Add(detail); + detail.Controls.Add(new DevExpress.XtraReports.UI.XRPdfContent { Source = _basePdfBytes, GenerateOwnPages = true }); + return report; + } + return CreateReportInstance(); + } + + static void AddAnnotationPlaceholders(XtraReport report, IReadOnlyList annotations) { + var bottomMargin = report.Bands.OfType().FirstOrDefault(); + if (bottomMargin is null) { + bottomMargin = new BottomMarginBand(); + report.Bands.Add(bottomMargin); + } + + const float sigWidth = 230F; + const float sigHeight = 154F; + const float bottomPad = 6F; + const float defaultTopPad = 8F; + const float maxBandHeight = 210F; + + float requiredHeight = defaultTopPad + sigHeight + bottomPad; + bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight)); + float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - sigHeight); + + foreach (var ann in annotations) { + float sigX = (float)(ann.X); + var annotId = ann.Id.ToString(); + + var placeholder = new XRLabel { + Name = $"receiverSignaturePlaceholder_{annotId}", + Text = "\u270e Bitte unterschreiben", + BoundsF = new RectangleF(sigX, topPad, sigWidth, sigHeight), + Borders = BorderSide.All, + BorderColor = System.Drawing.Color.FromArgb(230, 81, 0), + BackColor = System.Drawing.Color.FromArgb(30, 255, 236, 153), + Font = new DXFont("Open Sans", 10F, DXFontStyle.Regular), + ForeColor = System.Drawing.Color.FromArgb(94, 38, 0), + TextAlignment = TextAlignment.MiddleCenter + }; + + bottomMargin.Controls.Add(placeholder); + } + } + + XtraReport CreateReportInstance() { + return ReportStorage.TryGetReport("LargeDatasetReport", out var savedReport) + ? savedReport + : PredefinedReports.ReportsFactory.GetReport("LargeDatasetReport"); + } + + static void AddSignature(XtraReport report, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) { + var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]); + using var imageStream = new MemoryStream(imageBytes); + var imageSource = new ImageSource(DXImage.FromStream(imageStream)); + var bottomMargin = report.Bands.OfType().FirstOrDefault(); + + if(bottomMargin is null) { + bottomMargin = new BottomMarginBand(); + report.Bands.Add(bottomMargin); + } + + RemoveExistingSignature(bottomMargin); + + // Layout constants + const float sigX = 390F; + const float sigWidth = 230F; + const float sigImgHeight = 70F; + const float infoHeight = 65F; // up to 4 lines at 8pt + const float innerGap = 5F; + const float bottomPad = 6F; + const float defaultTopPad = 8F; + const float maxBandHeight = 210F; + + float requiredHeight = defaultTopPad + sigImgHeight + innerGap + infoHeight + bottomPad; + + // Grow band if needed, but cap at maxBandHeight to avoid overlapping page content + bottomMargin.HeightF = Math.Min(maxBandHeight, Math.Max(bottomMargin.HeightF, requiredHeight)); + + // If band is tighter than required, compress top padding so content still fits + float topPad = Math.Max(0F, bottomMargin.HeightF - bottomPad - infoHeight - innerGap - sigImgHeight); + + float imageY = topPad; + float labelY = imageY + sigImgHeight + innerGap; + + var signatureInformation = string.IsNullOrWhiteSpace(signerPosition) + ? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}" + : $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}"; + + var signature = new XRPictureBox { + Name = "receiverSignatureImage", + ImageSource = imageSource, + BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight), + Sizing = ImageSizeMode.ZoomImage, + Borders = BorderSide.Bottom, + BorderColor = System.Drawing.Color.FromArgb(73, 80, 87) + }; + + var signatureLabel = new XRLabel { + Name = "receiverSignatureLabel", + Text = signatureInformation, + Multiline = true, + BoundsF = new RectangleF(sigX, labelY, sigWidth, infoHeight), + Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular), + ForeColor = System.Drawing.Color.FromArgb(73, 80, 87), + TextAlignment = TextAlignment.TopLeft + }; + + bottomMargin.Controls.AddRange(new XRControl[] { signature, signatureLabel }); + } + + static void RemoveExistingSignature(BottomMarginBand bottomMargin) { + var controls = bottomMargin.Controls + .Cast() + .Where(control => control.Name is "receiverSignatureLabel" or "receiverSignatureImage") + .ToArray(); + + foreach(var control in controls) + bottomMargin.Controls.Remove(control); + } + + static void AddSignatureAtAnnotation(XtraReport report, AnnotationDto? annotation, string signatureDataUrl, string signerFullName, string signerPosition, string signaturePlace) { + var imageBytes = Convert.FromBase64String(signatureDataUrl[(signatureDataUrl.IndexOf(',') + 1)..]); + using var imageStream = new MemoryStream(imageBytes); + var imageSource = new ImageSource(DXImage.FromStream(imageStream)); + + var detail = report.Bands.OfType().FirstOrDefault(); + if (detail is null) return; + + var annotId = annotation?.Id.ToString() ?? "0"; + RemoveExistingSignatureById(detail, annotId); + + const float sigWidth = 230F; + const float sigImgHeight = 70F; + const float infoHeight = 48.75F; + const float innerGap = 5F; + + float sigX = (float)(annotation?.X ?? 390.0); + float imageY = (float)(annotation?.Y ?? 900.0); + float labelY = imageY + sigImgHeight + innerGap; + int targetPage = annotation?.Page ?? 1; + + var signatureInformation = string.IsNullOrWhiteSpace(signerPosition) + ? $"{signerFullName}\n{signaturePlace}, {DateTime.Now:d}" + : $"{signerFullName}\n{signerPosition}\n{signaturePlace}, {DateTime.Now:d}"; + + var signature = new XRPictureBox { + Name = $"receiverSignatureImage_{annotId}", + ImageSource = imageSource, + BoundsF = new RectangleF(sigX, imageY, sigWidth, sigImgHeight), + Sizing = ImageSizeMode.ZoomImage, + Borders = BorderSide.Bottom, + BorderColor = System.Drawing.Color.FromArgb(73, 80, 87), + BackColor = Color.FromArgb(219, 219, 219) + }; + + var signatureLabel = new XRLabel { + Name = $"receiverSignatureLabel_{annotId}", + Text = signatureInformation, + Multiline = true, + BoundsF = new RectangleF(sigX, labelY - 5, sigWidth, infoHeight), + Font = new DXFont("Open Sans", 8F, DXFontStyle.Regular), + ForeColor = System.Drawing.Color.FromArgb(73, 80, 87), + TextAlignment = TextAlignment.TopCenter, + BackColor = Color.FromArgb(219, 219, 219) + }; + + // Show each control only on the target page using an independent print counter + int sigPrintCount = 0; + signature.BeforePrint += (_, e) => { + sigPrintCount++; + e.Cancel = sigPrintCount != targetPage; + }; + + int lblPrintCount = 0; + signatureLabel.BeforePrint += (_, e) => { + lblPrintCount++; + e.Cancel = lblPrintCount != targetPage; + }; + + detail.Controls.AddRange(new XRControl[] { signature, signatureLabel }); + } + + static void RemoveExistingSignatureById(DevExpress.XtraReports.UI.DetailBand detail, string annotId) { + var controls = detail.Controls + .Cast() + .Where(c => c.Name == $"receiverSignatureImage_{annotId}" || c.Name == $"receiverSignatureLabel_{annotId}") + .ToArray(); + foreach (var c in controls) + detail.Controls.Remove(c); + } + + public void Dispose() { + _dotNetRef?.Dispose(); + } +} +