Extension Development

This chapter lists important integration events and examples to help you develop an extension for Freeze for MSDynamics 365 Business Central

Important:
Adaptations may only be carried out by appropriately trained consultants/developers. Furthermore, these are always outside the standard support provided by DEXPRO.

Attachment Download

The DXP Freeze Result Management codeunit provides two primary methods for downloading archived attachments as ZIP files:

  1. DownloadAttachmentsAsZipWithPagination - Downloads attachments from search query results with pagination support
  2. DownloadAttachmentsAsZipFromRecord - Downloads attachments from specific Business Central records

Both methods organize attachments into structured ZIP archives with comprehensive metadata for audit and tracking purposes.

Method 1: DownloadAttachmentsAsZipWithPagination

Purpose

Downloads all attachments from a search query result set, processing results page by page to handle large datasets efficiently. Each record’s attachments are organized into individual ZIP files within a main ZIP archive.

Overloads Available

1. Basic Usage

procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer): Boolean

2. With Filters

procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text): Boolean

3. Full Control

procedure DownloadAttachmentsAsZipWithPagination(SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; StoreApiLink: Text; RecordsPerPage: Integer; SuppressDialog: Boolean): Boolean

4. Pre-populated Records (Advanced)

procedure DownloadAttachmentsAsZipWithPagination(var TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary; var TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary; var TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary; var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; SearchQuery: Text; var ZipArchive: Codeunit "Data Compression"; var AttachmentCount: Integer; FilenameFilter: Text; FileExtensionFilter: Text; SuppressDialog: Boolean): Boolean

Parameters

Parameter Type Description
SearchQuery Text Freeze search query string. If empty in pre-populated overload, uses existing query or re-executes search
ZipArchive Codeunit “Data Compression” ZIP archive object that will contain the downloaded files
AttachmentCount Integer (var) Returns the total number of attachments downloaded
FilenameFilter Text Filter for attachment filenames (e.g., ‘.pdf’, 'invoice’)
FileExtensionFilter Text Filter for file extensions (e.g., ‘pdf’, ‘docx’)
StoreApiLink Text Optional specific store API link
RecordsPerPage Integer Number of records per page (default: 100)
SuppressDialog Boolean Whether to suppress progress dialog

Return Value

  • Booleantrue if attachments were found and downloaded; false otherwise

Example Usage

Basic Download

procedure DownloadSearchResults()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    FileMgt: Codeunit "File Management";
    TempBlob: Codeunit "Temp Blob";
    AttachmentCount: Integer;
    InStr: InStream;
    OutStr: OutStream;
    SearchQuery: Text;
begin
    SearchQuery := 'invoice AND 2024';
    
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
        // Save ZIP to file
        TempBlob.CreateOutStream(OutStr);
        ZipArchive.SaveZipArchive(OutStr);
        TempBlob.CreateInStream(InStr);
        
        FileMgt.DownloadFromStreamHandler(InStr, '', '', '', 'SearchResults.zip');
        Message('Downloaded %1 attachments successfully.', AttachmentCount);
    end else
        Message('No attachments found for the search query.');
end;

With Filters

procedure DownloadPDFInvoices()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    AttachmentCount: Integer;
    SearchQuery: Text;
begin
    SearchQuery := 'type:invoice';
    
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(
        SearchQuery, 
        ZipArchive, 
        AttachmentCount, 
        '*.pdf', // Only PDF files
        'pdf'    // File extension filter
    ) then begin
        // Process the ZIP archive
        ProcessDownloadedFiles(ZipArchive, AttachmentCount);
    end;
end;

Using Pre-populated Records

procedure DownloadFromExistingResults()
var
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    TempFrzResultQueryHeader: Record "DXP FRZ Query Result Header" temporary;
    TempFrzResultRecordHeader: Record "DXP FRZ Record Result Header" temporary;
    TempFrzResultRecordField: Record "DXP FRZ Result Record-Field" temporary;
    TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary;
    ZipArchive: Codeunit "Data Compression";
    AttachmentCount: Integer;
begin
    // Assume these records are already populated from a previous search
    PopulateSearchResults(TempFrzResultQueryHeader, TempFrzResultRecordHeader, TempFrzResultRecordField, TempFrzAttachmentResult);
    
    // Download using existing results without re-executing search
    if ResultMgt.DownloadAttachmentsAsZipWithPagination(
        TempFrzResultQueryHeader,
        TempFrzResultRecordHeader,
        TempFrzResultRecordField,
        TempFrzAttachmentResult,
        'invoice search', // SearchQuery - if empty, will re-execute search
        ZipArchive,
        AttachmentCount,
        '',     // No filename filter
        '',     // No extension filter
        true    // Suppress dialog
    ) then begin
        ProcessDownloadedFiles(ZipArchive, AttachmentCount);
    end;
end;

ZIP Structure (Pagination)

SearchResults.zip
├── export-metadata.json
├── Invoice_001_V1_20241201_1430.zip
│   ├── {GUID}_invoice.pdf
│   └── {GUID}_supporting_doc.docx
├── PurchaseOrder_002_V2_20241202_0900.zip
│   └── {GUID}_po_document.pdf
└── Contract_003_V1_20241203_1200.zip
    ├── {GUID}_contract.pdf
    └── {GUID}_amendment.pdf

Method 2: DownloadAttachmentsAsZipFromRecord

Purpose

Downloads attachments from specific Business Central records. Each selected record’s attachments are organized into individual ZIP files within a main ZIP archive.

Overloads Available

1. Basic Usage

procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"): Boolean

2. With Filters

procedure DownloadAttachmentsAsZipFromRecord(var SelectedRecord: RecordRef; var ZipArchive: Codeunit "Data Compression"; FilenameFilter: Text; FileExtensionFilter: Text): Boolean

Parameters

Parameter Type Description
SelectedRecord RecordRef (var) RecordRef containing the selected Business Central records
ZipArchive Codeunit “Data Compression” ZIP archive object that will contain the downloaded files
FilenameFilter Text Filter for attachment filenames
FileExtensionFilter Text Filter for file extensions

Return Value

  • Booleantrue if attachments were found and downloaded; false otherwise

Example Usage

Download from Sales Invoices

procedure DownloadInvoiceAttachments()
var
    SalesInvoiceHeader: Record "Sales Invoice Header";
    ResultMgt: Codeunit "DXP FRZ Result Mgt.";
    ZipArchive: Codeunit "Data Compression";
    RecordRef: RecordRef;
    HasAttachments: Boolean;
begin
    // Select specific invoices
    SalesInvoiceHeader.SetRange("Posting Date", DMY2Date(1, 1, 2024), DMY2Date(31, 12, 2024));
    SalesInvoiceHeader.SetFilter("Sell-to Customer No.", '10000|20000');
    
    if SalesInvoiceHeader.FindSet() then begin
        RecordRef.GetTable(SalesInvoiceHeader);
        
        HasAttachments := ResultMgt.DownloadAttachmentsAsZipFromRecord(RecordRef, ZipArchive);
        
        if HasAttachments then
            SaveZipFile(ZipArchive, 'InvoiceAttachments.zip')
        else
            Message('No attachments found for the selected invoices.');
    end;
end;

Download with Filters from Page

// In a page extension
action(DownloadAttachmentsFiltered)
{
    Caption = 'Download Filtered Attachments';
    Image = ExportFile;
    
    trigger OnAction()
    var
        ResultMgt: Codeunit "DXP FRZ Result Mgt.";
        ZipArchive: Codeunit "Data Compression";
        RecordRef: RecordRef;
        FilenameFilter: Text;
        FileExtensionFilter: Text;
    begin
        // Show filter dialog
        if ShowFilterDialog(FilenameFilter, FileExtensionFilter) then begin
            CurrPage.SetSelectionFilter(Rec);
            RecordRef.GetTable(Rec);
            
            if ResultMgt.DownloadAttachmentsAsZipFromRecord(
                RecordRef, 
                ZipArchive, 
                FilenameFilter, 
                FileExtensionFilter
            ) then
                DownloadZipFile(ZipArchive, 'FilteredAttachments.zip');
        end;
    end;
}

ZIP Structure (Records)

RecordAttachments.zip
├── export-metadata.json
├── Sales_Invoice_Header_Company_SI-001.zip
│   ├── {GUID}_invoice.pdf
│   └── {GUID}_terms.pdf
├── Sales_Invoice_Header_Company_SI-002.zip
│   └── {GUID}_invoice.pdf
└── Sales_Invoice_Header_Company_SI-003.zip
    ├── {GUID}_invoice.pdf
    ├── {GUID}_delivery_note.pdf
    └── {GUID}_receipt.jpg

Metadata Structure

Both methods generate comprehensive metadata in export-metadata.json:

Pagination Export Metadata

{
  "exportInfo": {
    "exportTimestamp": "2024-12-19T10:13:52.248Z",
    "exportedBy": "USER001",
    "searchQuery": "type:invoice AND year:2024",
    "totalRecordsFound": 150,
    "totalPages": 15,
    "exportType": "paginated-search",
    "description": "Freeze Search Query Export"
  },
  "appliedFilters": {
    "filenameFilter": "*.pdf",
    "fileExtensionFilter": "pdf"
  },
  "statistics": {
    "pagesProcessed": 15,
    "totalRecordsProcessed": 150,
    "recordsWithAttachments": 120,
    "recordsWithoutAttachments": 30,
    "totalAttachments": 245,
    "exportCompletedAt": "2024-12-19T10:15:33.021Z"
  },
  "records": [
    {
      "recordId": "{GUID}",
      "title": "Invoice INV-2024-001",
      "version": 1,
      "archivedAt": "2024-12-01T09:30:00Z",
      "archivedBy": "SYSTEM",
      "type": "Sales Invoice",
      "masterId": "{GUID}",
      "attachmentCount": 3,
      "hasAttachments": true,
      "zipFile": "Invoice_INV-2024-001_V1_20241201_0930.zip"
    }
  ]
}

Record Export Metadata

{
  "exportTimestamp": "2024-12-19T14:30:00Z",
  "exportedBy": "USER001",
  "totalRecordsProcessed": 25,
  "description": "DXP Freeze Attachments Export",
  "sourceTable": {
    "tableNumber": 112,
    "tableName": "Sales Invoice Header",
    "tableCaption": "Posted Sales Invoice"
  },
  "appliedFilters": {
    "filenameFilter": "*.pdf",
    "fileExtensionFilter": "pdf"
  },
  "statistics": {
    "totalAttachments": 45,
    "recordsWithAttachments": 20,
    "recordsWithoutAttachments": 5,
    "totalZipFiles": 20
  },
  "records": [
    {
      "recordId": "Sales Invoice Header: Company, SI-001",
      "systemId": "{GUID}",
      "primaryKey": {
        "fields": [
          {
            "fieldName": "No.",
            "fieldValue": "SI-001",
            "fieldType": "Code"
          }
        ]
      },
      "hasAttachments": true,
      "attachmentCount": 2,
      "zipFile": "Sales_Invoice_Header_Company_SI-001.zip"
    }
  ]
}

Performance Considerations

Pagination Method

  • Large Result Sets: Automatically handles pagination to process large datasets efficiently
  • Memory Management: Processes one page at a time, clearing memory between pages
  • Progress Tracking: Shows real-time progress for long-running operations
  • Recommended For: Search queries that may return hundreds or thousands of records

Record Method

  • Selected Records: Processes only the records you specifically select
  • Direct Processing: No pagination overhead for smaller datasets
  • Batch Processing: Efficient for processing specific record sets
  • Recommended For: Targeted downloads from specific Business Central records

Error Handling

Both methods include comprehensive error handling:

Common Scenarios

  • No Results Found: Returns false when no records or attachments are found
  • Permission Issues: Automatically excludes records the user cannot access
  • API Failures: Gracefully handles API communication errors
  • Empty Filters: Handles empty or invalid filter parameters

Best Practices

// Always check return value
if not ResultMgt.DownloadAttachmentsAsZipWithPagination(SearchQuery, ZipArchive, AttachmentCount) then begin
    Message('No attachments found or download failed.');
    exit;
end;

// Validate attachment count
if AttachmentCount = 0 then begin
    Message('Search completed but no attachments matched the criteria.');
    exit;
end;

// Handle large downloads
if AttachmentCount > 1000 then
    if not Confirm('This will download %1 attachments. Continue?', false, AttachmentCount) then
        exit;

Integration Events

Both methods support integration events for customization:

Available Events

  • OnBeforeDownloadAttachmentsAsZip: Modify behavior before download starts
  • OnBeforeProcessAttachmentForZip: Skip or modify individual attachments
  • OnAfterGetAttachmentBase64: Modify attachment content after retrieval
  • OnAfterAddAttachmentToZip: Perform actions after adding to ZIP
  • OnNoAttachmentsFound: Handle no attachments scenario

Example Integration

[EventSubscriber(ObjectType::Codeunit, Codeunit::"DXP FRZ Result Mgt.", 'OnBeforeProcessAttachmentForZip', '', false, false)]
local procedure OnBeforeProcessAttachmentForZip(var TempFrzAttachmentResult: Record "DXP FRZ Attachment Result" temporary; var IsHandled: Boolean)
begin
    // Skip attachments larger than 10MB
    if TempFrzAttachmentResult.Filesize > 10485760 then
        IsHandled := true;
end;

File Naming Conventions

Automatic Sanitization

All filenames are automatically sanitized using the SanitizeFileName method:

  • Invalid characters (< > : " / \ | ? *) are replaced with underscores
  • Spaces are replaced with underscores
  • Maximum filename lengths are enforced

Unique Naming

  • Individual Files: Include attachment GUID prefix to ensure uniqueness
  • ZIP Files: Include record information and timestamps
  • No Conflicts: Guaranteed unique names within each ZIP archive

Finding an archived document via search

Finding an archived document via search

The Attachment Download page describes how to download the attachments of an archive record you have already found. This page covers the step before that: how to find the matching archive record – including cross-company, e.g. in an intercompany scenario to locate the sales document of the partner company from a purchase invoice.

Process

  1. Build a search term (Lucene query)
  2. Submit the search to Freeze
  3. Load the result and its attachments into temporary tables
  4. Download the attachments (see the "Attachment Download" page)

Public procedures used

DXP Freeze Search Mgt. (Codeunit 70954893)

Procedure Purpose
GetSearchCombinationForTable(SystemId: Guid; TableNo: Integer): Text Ready-made query for a local document (SysLink + customer/vendor + current company).
ToSafeSearchTerm(OldSearchTerm: Text): Text Lucene escaping + URL encoding of a search value. Use for every dynamic value.
SystemLinkSearchQuery(SystemId: Guid; TableNo: Integer): Text Building block SysLink:{GUID}-{TableNo}.

Cross-company: GetSearchCombinationForTable cannot be used here – it requires the SystemId of the original document and automatically appends the current CompanyName(). To search in another company, build your own query and set the partner company explicitly via the Company field.

DXP Freeze API Mgt. (Codeunit 70954891)

Procedure Purpose
SubmitSearch(SearchQuery: Text): JsonObject Submit a search (1 result/page).
SubmitSearch(SearchQuery: Text; ItemsPerPage: Integer): JsonObject Search with page size.
SubmitSearch(SearchQuery: Text; StoreName: Text[30]): JsonObject Search in a specific store.

DXP FRZ Result Mgt. (Codeunit 70954894)

Procedure Purpose
CreateResultForAttachments(QueryResult: JsonObject; var TempFrzResultRecordHeader; var TempFrzAttachmentResult; SearchQueryTxt: Text) Loads results + attachment metadata from the JSON into temporary tables.
HasRecord(SearchString: Text): Boolean Checks whether there is any result at all.
GetEffectiveResultsCount(SearchString: Text): Integer Returns the number of results.

Query structure

A query consists of FieldName:"Value" pairs joined with AND / OR. Field names use _ instead of spaces/special characters (e.g. Sell_to_Customer_No). Always escape values via ToSafeSearchTerm.

Example – sales invoice by document number, document type and company:

(No:"SO-2026-001" AND Document_Type:"2" AND Company:"SALES INC")

Document_Type:"2" corresponds to the Invoice document type.

Code example

procedure FindSalesDocInArchive(SalesDocNo: Code[20]; PartnerCompany: Text): Boolean
var
    TempResultHeader: Record "DXP FRZ Record Result Header" temporary;
    TempAttachment: Record "DXP FRZ Attachment Result" temporary;
    FrzApiMgt: Codeunit "DXP Freeze API Mgt.";
    FrzResultMgt: Codeunit "DXP FRZ Result Mgt.";
    FrzSearchMgt: Codeunit "DXP Freeze Search Mgt.";
    QueryResult: JsonObject;
    SearchQuery: Text;
    Base64: Text;
begin
    // 1) Build the query (cross-company)
    SearchQuery := StrSubstNo(
        '(No:"%1" AND Document_Type:"2" AND Company:"%2")',
        FrzSearchMgt.ToSafeSearchTerm(SalesDocNo),
        FrzSearchMgt.ToSafeSearchTerm(PartnerCompany));

    // 2) Submit the search
    QueryResult := FrzApiMgt.SubmitSearch(SearchQuery, 50);

    // 3) Load results + attachments
    FrzResultMgt.CreateResultForAttachments(
        QueryResult, TempResultHeader, TempAttachment, SearchQuery);
    if TempAttachment.IsEmpty() then
        exit(false);

    // 4) Fetch the attachment as Base64 (see "Attachment Download" page)
    TempAttachment.FindSet();
    repeat
        Base64 := FrzApiMgt.GetSpecificRecordAttachment(
            TempAttachment."File Link",
            TempAttachment.ID,
            TempAttachment.Filename,
            false); // false = no direct browser download
        // ... process Base64 (e.g. store as document attachment)
    until TempAttachment.Next() = 0;

    exit(true);
end;

Note: Storing into the document attachment (table "Document Attachment", 1173) is standard Business Central functionality and not part of the Freeze module.