There were some older posts on this topic that pertained to classic customizations but I couldn’t find a version that worked for kinetic. For reference this was all done on 2025.1.8.
The Business Problem
We have dashboards setup for end users to have the information they need on a single page where they can do a majority of their work, but they need to be able to add and view attachments. Out of the box apps (Project Entry, Opportunity/Quote Entry) have built in support for attachments but are mostly obscured as far as how they’re setup. I needed to allow users to attach files without leaving their dashboards.
Overall Flow
We have ECM set up as our file attachment service so this is particular to the docstar methods, but could be adapted.
In Application Studio, add a file-picker-client widget to let the user choose local files.
File transfer widget in an event to upload those files to the server, I just put them in the User Data for temporary storage and delete them from there in a later step.
Call a function that does the next three steps.
Download the file from the Server/User Data and uploads it to ECM using the DocStarUploadFile method.
Create XFileRef record (this ties the ECM Guid to the Kinetic XFileRefNum)
Create XFileAttch record (this ties the XFileRefNum to the correct Kinetic Record)
To view the attachments:
I created a baq from xFileRef and XFileAttch to show the attachments for a record (like a project or quote) in a grid and then set the reference number as a link.
When clicked I call another function to download the base64 file contents using the DocStarDownloadFile method and return the base64file to the browser
then I use a row-update widget (credit to @josecgomez) to send that base64 data to a website widget control and display the attachments.
Here’s the function library that has both of these functions AttachmentsLibrary.efxb (10.0 KB)
Here’s the row-update expression that will probably need tweaking for your specific use.
Any chance you could post the actual function code snippets on here?
Tried to import the function library but running an older version and I’m getting a error.
@JPerry sure! The library will need to have ICE:BO:Attachment and ICE:BO:XFileRef services as references and I also have Ice.Contracts.BO.Attachment.dll as an assembly reference, but I don’t remember for sure if I added that during testing and ended up not needing it.
Here’s the code for adding attachments:
Add Attachments
List<string> failedUploads = new List<string>();
var fileNames = OriginalFileName.Split('|');
foreach (var fileName in fileNames)
{
var trimmedFileName = fileName.Trim();
if (string.IsNullOrEmpty(trimmedFileName))
continue;
try
{
// Step 1: Build path (UserData root)
ServerFolder folder = ServerFolder.UserData;
var filePath = new FilePath(folder, "");
filePath.Combine(trimmedFileName);
// --- Wait for the file to be present & readable (no helper methods) ---
const int maxMs = 10000; // total wait up to 10s
const int stepMs = 100; // poll every 100ms
int waited = 0;
bool readOk = false;
byte[] fileBytes = null;
while (waited <= maxMs)
{
if (this.Sandbox.IO.File.Exists(filePath))
{
try
{
// try to read; if still being written, this may throw
fileBytes = this.Sandbox.IO.File.ReadAllBytes(filePath);
readOk = true;
break;
}
catch
{
// not ready yet; fall through to sleep
}
}
System.Threading.Thread.Sleep(stepMs);
waited += stepMs;
}
if (!readOk)
{
failedUploads.Add(trimmedFileName + " (server file not ready)");
continue;
}
// Step 2: Check if file already exists in ECM
bool fileExists = false;
this.CallService<Ice.Contracts.AttachmentSvcContract>(svc =>
{
string xFileName;
int xFileNum;
int result = svc.DocStarFileExistsForTableRow(
DocTypeID,
TableName.Split('.').Last(),
trimmedFileName,
Guid.Parse(ForeignSysRowID),
out xFileName,
out xFileNum
);
fileExists = result != 0;
});
if (fileExists)
{
failedUploads.Add(trimmedFileName + " (already exists)");
continue;
}
// Step 3: Upload to ECM
string ecmPath = string.Empty;
var metadata = new Dictionary<string, string>
{
{ "Keywords", Keywords },
{ "_Category", string.IsNullOrWhiteSpace(Category) ? "" : Category },
{ "_TableName", TableName },
{ "_TableSysRowID", ForeignSysRowID }
};
this.CallService<Ice.Contracts.AttachmentSvcContract>(svc =>
{
ecmPath = svc.DocStarUploadFile(
trimmedFileName,
fileBytes,
DocTypeID,
TableName.Split('.').Last(),
metadata
);
});
// Step 4: Create XFileRef
int createdXFileRefNum = 0;
var xFileRefTS = new Ice.Tablesets.XFileRefTableset();
this.CallService<Ice.Contracts.XFileRefSvcContract>(svc =>
{
svc.GetNewXFileRef(ref xFileRefTS);
var xref = xFileRefTS.XFileRef[0];
xref.Company = Company;
xref.XFileRefNum = 0;
xref.XFileName = ecmPath; // ECM returned path (GUID;\\Company\\Table\\File)
xref.BaseFileName = trimmedFileName; // just the filename
xref.XFileDesc = string.IsNullOrWhiteSpace(DrawDesc) ? "Uploaded via Generic Function" : DrawDesc;
xref.DocTypeID = string.IsNullOrWhiteSpace(DocTypeID) ? "" : DocTypeID;
xref.ExternalSystemDoc = "";
xref.RowMod = "A";
svc.Update(ref xFileRefTS);
createdXFileRefNum = xFileRefTS.XFileRef[0].XFileRefNum;
});
// Step 5: Create XFileAttch (link XFileRef to record)
var xFileAttchDS = new Ice.Tablesets.AttachmentTableset();
this.CallService<Ice.Contracts.AttachmentSvcContract>(svc =>
{
svc.GetNewXFileAttch(ref xFileAttchDS, TableName.Split('.')[0], TableName.Split('.')[1], Guid.Parse(ForeignSysRowID));
var xattch = xFileAttchDS.XFileAttch[0];
xattch.Company = Company;
xattch.RelatedToSchemaName = TableName.Split('.')[0]; // e.g., "Erp"
xattch.RelatedToFile = TableName.Split('.')[1]; // e.g., "Customer"
xattch.ForeignSysRowID = Guid.Parse(ForeignSysRowID);
xattch.XFileRefNum = createdXFileRefNum;
xattch.RowMod = "A";
svc.Update(ref xFileAttchDS);
});
// Step 6: Delete file from disk (only after successful upload + attach)
this.Sandbox.IO.File.Delete(filePath);
}
catch (Exception ex)
{
failedUploads.Add(trimmedFileName + " (error: " + ex.Message + ")");
}
}
// Final return message
ResultMessage = failedUploads.Count == 0
? "All files uploaded successfully."
: "Some files failed:\r\n" + string.Join("\r\n", failedUploads);
And Here’s the code for Downloading Attachments
Download Attachments
// Validate inputs
if (XFileRefNum <= 0) throw new Ice.BLException("XFileRefNum is required and must be > 0.");
if (string.IsNullOrWhiteSpace(FullTableName)) throw new Ice.BLException("FullTableName is required (e.g., 'Erp.Project').");
if (string.IsNullOrWhiteSpace(TableSysRowID)) throw new Ice.BLException("_TableSysRowID (TableSysRowID) is required.");
// Build metadata exactly like the UI does for download
var md = new Dictionary<string, string>
{};
// Call AttachmentSvc.DocStarDownloadFile
byte[] bytes = null;
this.CallService<Ice.Contracts.AttachmentSvcContract>(svc =>
{
// DocStarDownloadFile signature: (int xFileRefNum, ref Dictionary<string,string> metadata) -> byte[]
bytes = svc.DocStarDownloadFile(XFileRefNum, ref md);
});
if (bytes == null || bytes.Length == 0)
throw new Ice.BLException("Download returned no content.");
// Convert to Base64 for return to client
FileBase64 = Convert.ToBase64String(bytes);
// Optional: pull back filename/content type if DocStar filled them
string fn;
if (md.TryGetValue("FileName", out fn)) FileName = fn;
string ct;
if (md.TryGetValue("ContentType", out ct)) ContentType = ct;
// Friendly message
ResultMessage = $"Downloaded { (string.IsNullOrEmpty(FileName) ? "file" : FileName) } ({bytes.Length} bytes) successfully.";