Control Add-in Architecture

This document provides an in-depth technical overview of the Control Add-in architecture that powers the Drag & Drop extension's client-side functionality in Business Central.

Control Add-in Overview

Architectural Foundation

Control Add-in Framework: The Drag & Drop extension leverages Business Central's Control Add-in framework to bridge web technologies with the AL environment:

  • Client-Side Components: JavaScript and CSS for user interface
  • Server-Side Integration: AL codeunit for business logic processing
  • Communication Bridge: Extensibility methods for data exchange
  • Event Management: Bi-directional event handling between client and server

Technology Stack:

┌─────────────────────────────────────────┐
│             User Interface              │
│        (Browser + Drag & Drop)          │
├─────────────────────────────────────────┤
│           Client-Side Layer             │
│     JavaScript + CSS + HTML            │
├─────────────────────────────────────────┤
│         Control Add-in Bridge           │
│       Business Central Framework       │
├─────────────────────────────────────────┤
│          Server-Side Layer              │
│           AL Codeunits                  │
├─────────────────────────────────────────┤
│          Business Central               │
│         Database Layer                  │
└─────────────────────────────────────────┘

Client-Side Architecture

JavaScript Components

DropArea.js - Core Functionality:

// Core drag and drop event handling structure
class DropAreaManager {
    constructor() {
        this.fileQueue = [];
        this.isUploading = false;
        this.currentContext = null;
    }
    
    // Event handlers for drag and drop lifecycle
    handleDragEnter(event) { /* Visual feedback */ }
    handleDragOver(event) { /* Drag validation */ }
    handleDragLeave(event) { /* Reset UI state */ }
    handleDrop(event) { /* File processing */ }
    
    // File processing and AL communication
    processFiles(files) { /* File reading and transmission */ }
    sendToAL(fileData) { /* AL method invocation */ }
}

Key JavaScript Capabilities: - File Reading: FileReader API for client-side file processing - Data Conversion: Base64 encoding for AL transmission compatibility - Event Management: DOM event handling for drag-and-drop lifecycle - State Management: UI state tracking and visual feedback coordination - Error Handling: Graceful degradation and user feedback

startup.js - Initialization:

// Control readiness signaling
function initializeDropArea() {
    // Initialize drag and drop handlers
    setupEventListeners();
    
    // Signal AL that control is ready
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('OnControlReady', []);
}

// Browser compatibility detection
function checkBrowserSupport() {
    return (
        'FileReader' in window &&
        'File' in window &&
        'FileList' in window
    );
}

CSS Architecture

DropArea.css - Visual Framework:

/* Base drop area styling */
.qteam-drop-area {
    position: relative;
    border: 2px dashed #ccc;
    border-radius: 8px;
    padding: 20px;
    text-align: center;
    transition: all 0.3s ease;
}

/* Interactive states */
.qteam-drop-area.drag-over {
    border-color: #007acc;
    background-color: #f0f8ff;
}

.qteam-drop-area.uploading {
    opacity: 0.7;
    pointer-events: none;
}

/* Responsive design considerations */
@media (max-width: 768px) {
    .qteam-drop-area {
        padding: 15px;
        font-size: 14px;
    }
}

Visual Design Principles: - Accessibility: WCAG compliance for inclusive design - Responsiveness: Adaptive layout for various screen sizes - Visual Feedback: Clear indication of interactive states - Business Central Integration: Consistent with Business Central visual language

Server-Side Architecture

AL Control Add-in Definition

QTEAMDropAreaControl.controladdin.al:

controladdin "QTEAM DropArea Control"
{
    RequestedHeight = 200;
    MinimumHeight = 150;
    MaximumHeight = 300;
    RequestedWidth = 300;
    MinimumWidth = 250;
    HorizontalStretch = true;
    VerticalShrink = true;

    Scripts = 'js/DropArea.js', 'js/startup.js';
    StyleSheets = 'css/DropArea.css';

    // AL to JavaScript method calls
    procedure InitializeDropArea();
    procedure SetRecordContext(TableId: Integer; RecordId: Text; DocumentType: Text);
    
    // JavaScript to AL event declarations
    event OnControlReady();
    event FileDropBegin(FileName: Text; FileSize: Integer);
    event FileDrop(FileName: Text; FileData: Text; IsLastChunk: Boolean);
    event FileDropComplete();
    event OnError(ErrorMessage: Text);
}

Communication Architecture

Extensibility Method Patterns:

AL to JavaScript Communication:

// Setting record context from AL
procedure SetRecordData(TableId: Integer; RecordNo: Code[20]; DocType: Integer; DocTypeText: Text)
begin
    CurrPage.DropAreaControl.InitializeDropArea();
    CurrPage.DropAreaControl.SetRecordContext(TableId, RecordNo, DocTypeText);
end;

JavaScript to AL Communication:

// Invoking AL methods from JavaScript
function notifyFileUploadBegin(fileName, fileSize) {
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('FileDropBegin', [fileName, fileSize]);
}

function sendFileDataToAL(fileName, fileData, isLastChunk) {
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('FileDrop', [fileName, fileData, isLastChunk]);
}

Data Flow Architecture

File Upload Process Flow

Step 1: User Interaction

// User drops files on control
handleDrop(event) {
    event.preventDefault();
    const files = event.dataTransfer.files;
    this.processFileQueue(files);
}

Step 2: File Processing

// File reading and chunking
processFile(file) {
    const reader = new FileReader();
    reader.onload = (e) => {
        const base64Data = btoa(e.target.result);
        this.sendChunkToAL(file.name, base64Data);
    };
    reader.readAsBinaryString(file);
}

Step 3: AL Processing

// Server-side file handling
trigger FileDrop(FileName: Text; FileData: Text; IsLastChunk: Boolean)
begin
    DropAreaMgmt.SetFileData(FileName, FileData);
    if IsLastChunk then
        DropAreaMgmt.CommitFile();
end;

Step 4: Business Central Integration

// Document attachment creation
procedure CommitFile()
var
    DocumentAttachment: Record "Document Attachment";
begin
    DocumentAttachment.Init();
    DocumentAttachment."Table ID" := TableId;
    DocumentAttachment."No." := RecordNo;
    DocumentAttachment."Document Type" := DocumentType;
    DocumentAttachment."File Name" := FileName;
    DocumentAttachment."Document Reference ID".ImportStream(FileInStream, FileName);
    DocumentAttachment.Insert(true);
end;

Event Management Architecture

Event Lifecycle

Control Initialization Sequence:

  1. Page Load: Business Central loads page with FactBox
  2. Control Creation: Control Add-in instantiated in DOM
  3. Resource Loading: JavaScript and CSS files loaded
  4. Initialization: startup.js executes initialization
  5. Ready Signal: OnControlReady event sent to AL
  6. Context Setting: AL sets record context via JavaScript methods

File Upload Event Sequence:

  1. Drag Enter: Visual feedback begins
  2. Drag Over: Validation and continued feedback
  3. Drop: FileDropBegin event triggered
  4. Processing: Multiple FileDrop events with data chunks
  5. Completion: FileDropComplete event signaled
  6. AL Processing: Business logic execution and storage

Error Handling Architecture

Client-Side Error Management:

// Comprehensive error handling
handleUploadError(error, fileName) {
    const errorMessage = `Upload failed for ${fileName}: ${error.message}`;
    this.showUserError(errorMessage);
    Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('OnError', [errorMessage]);
}

// User feedback mechanisms
showUserError(message) {
    // Visual error indication
    this.updateDropAreaState('error');
    // Error message display
    this.displayMessage(message, 'error');
}

Server-Side Error Management:

// AL error handling and user notification
trigger OnError(ErrorMessage: Text)
begin
    Message('Upload Error: %1', ErrorMessage);
    // Additional error logging and processing
    LogError(ErrorMessage);
end;

Browser Compatibility Architecture

Cross-Browser Support Strategy

Feature Detection Pattern:

// Progressive enhancement approach
function initializeWithFeatureDetection() {
    if (!checkDragDropSupport()) {
        // Fallback to traditional file input
        setupFileInputFallback();
        return;
    }
    
    if (!checkFileAPISupport()) {
        // Limited functionality mode
        setupLimitedMode();
        return;
    }
    
    // Full feature initialization
    setupFullDragDrop();
}

Browser-Specific Optimizations:

// Browser-specific handling
const browserOptimizations = {
    chrome: {
        chunkSize: 1024 * 1024, // 1MB chunks
        useWebWorkers: true
    },
    firefox: {
        chunkSize: 512 * 1024, // 512KB chunks
        useWebWorkers: false
    },
    edge: {
        chunkSize: 1024 * 1024, // 1MB chunks
        useWebWorkers: true
    },
    safari: {
        chunkSize: 256 * 1024, // 256KB chunks
        useWebWorkers: false
    }
};

Performance Architecture

Client-Side Optimization

Memory Management:

// Efficient file processing
class FileProcessor {
    constructor() {
        this.maxConcurrentUploads = 3;
        this.chunkSize = this.getOptimalChunkSize();
    }
    
    // Memory-efficient file reading
    processLargeFile(file) {
        return new Promise((resolve) => {
            const chunks = Math.ceil(file.size / this.chunkSize);
            let currentChunk = 0;
            
            const processChunk = () => {
                const start = currentChunk * this.chunkSize;
                const end = Math.min(start + this.chunkSize, file.size);
                const chunk = file.slice(start, end);
                
                this.readChunk(chunk).then(() => {
                    currentChunk++;
                    if (currentChunk < chunks) {
                        // Use setTimeout for non-blocking processing
                        setTimeout(processChunk, 0);
                    } else {
                        resolve();
                    }
                });
            };
            
            processChunk();
        });
    }
}

Network Optimization:

// Optimized data transmission
function optimizeDataTransmission(data) {
    // Compression consideration
    if (data.length > COMPRESSION_THRESHOLD) {
        return compressData(data);
    }
    return data;
}

// Retry mechanism for network issues
async function sendWithRetry(data, maxRetries = 3) {
    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            await sendToAL(data);
            return;
        } catch (error) {
            if (attempt === maxRetries) throw error;
            await delay(attempt * 1000); // Exponential backoff
        }
    }
}

Server-Side Optimization

AL Performance Considerations:

// Optimized file processing
procedure ProcessFileData(FileData: Text)
var
    Base64Convert: Codeunit "Base64 Convert";
    TempBlob: Codeunit "Temp Blob";
    InStr: InStream;
begin
    // Efficient Base64 to binary conversion
    TempBlob.CreateInStream(InStr);
    Base64Convert.FromBase64(FileData, InStr);
    
    // Stream-based processing to minimize memory usage
    ProcessFileStream(InStr);
end;

Security Architecture

Client-Side Security

Input Validation:

// File validation before processing
function validateFile(file) {
    // File type validation
    if (!isAllowedFileType(file.type)) {
        throw new Error(`File type ${file.type} not allowed`);
    }
    
    // File size validation
    if (file.size > MAX_FILE_SIZE) {
        throw new Error(`File size exceeds maximum allowed size`);
    }
    
    // File name validation
    if (!isValidFileName(file.name)) {
        throw new Error('Invalid file name');
    }
    
    return true;
}

Data Sanitization:

// Secure data handling
function sanitizeFileName(fileName) {
    // Remove potentially dangerous characters
    return fileName.replace(/[<>:"/\\|?*]/g, '_');
}

function secureBase64Encode(data) {
    // Ensure proper Base64 encoding
    try {
        return btoa(data);
    } catch (error) {
        throw new Error('Failed to encode file data');
    }
}

Server-Side Security

AL Security Implementation:

// Server-side validation and security
procedure ValidateFileUpload(FileName: Text; FileSize: Integer): Boolean
begin
    // File name security check
    if not IsValidFileName(FileName) then
        Error('Invalid file name: %1', FileName);
    
    // Size validation
    if FileSize > GetMaxFileSize() then
        Error('File size exceeds limit: %1', FileSize);
    
    // User permission validation
    if not CheckUserUploadPermission() then
        Error('Insufficient permissions for file upload');
    
    exit(true);
end;

This comprehensive architecture documentation provides the technical foundation for understanding, maintaining, and extending the Control Add-in components of the Drag & Drop extension.