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:
- Page Load: Business Central loads page with FactBox
- Control Creation: Control Add-in instantiated in DOM
- Resource Loading: JavaScript and CSS files loaded
- Initialization: startup.js executes initialization
- Ready Signal: OnControlReady event sent to AL
- Context Setting: AL sets record context via JavaScript methods
File Upload Event Sequence:
- Drag Enter: Visual feedback begins
- Drag Over: Validation and continued feedback
- Drop: FileDropBegin event triggered
- Processing: Multiple FileDrop events with data chunks
- Completion: FileDropComplete event signaled
- 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.