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:
DownloadAttachmentsAsZipWithPagination- Downloads attachments from search query results with pagination supportDownloadAttachmentsAsZipFromRecord- 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
Boolean:trueif attachments were found and downloaded;falseotherwise
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
Boolean:trueif attachments were found and downloaded;falseotherwise
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
falsewhen 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 startsOnBeforeProcessAttachmentForZip: Skip or modify individual attachmentsOnAfterGetAttachmentBase64: Modify attachment content after retrievalOnAfterAddAttachmentToZip: Perform actions after adding to ZIPOnNoAttachmentsFound: 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
- Build a search term (Lucene query)
- Submit the search to Freeze
- Load the result and its attachments into temporary tables
- 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:
GetSearchCombinationForTablecannot be used here – it requires theSystemIdof the original document and automatically appends the currentCompanyName(). To search in another company, build your own query and set the partner company explicitly via theCompanyfield.
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.