🚀 Welcome to MDriven Learn –  MDriven is now on Discord!  Don’t miss the latest Release Notes.
Multiple file upload component
This page was created by Wikiadmin on 2026-07-29. Last edited by Wikiadmin on 2026-07-29.

You can add a custom MDriven Turnkey component that lets a user select several files and uploads them one at a time into newly created model objects.

When to use this component

Use this pattern when one browser file-picker operation must create several related objects. For example, a Project has many Document objects, and each selected file must become a separate Document with its own Blob and file name.

A standard Blob upload column uploads one file to its current target. This component changes that target before each upload: it runs a ViewModel action that creates a Document, assigns the new Document to a temporary ViewModel variable, and then uploads the next file to that variable.

The component is an EXT Component. Its markup defines the file input, and its JavaScript reads the selected files and coordinates ViewModel actions and uploads.

How the upload works

The component processes the selected files in this order:

  1. The user selects multiple files in the browser file picker.
  2. The component reads each selected file in the browser.
  3. Before uploading a file, the component executes an action such as AddDocumentToProject.
  4. The action creates a new Document, adds it to the current Project, and assigns it to vUploadFileTarget.
  5. The component uploads the file to the Blob column that resolves to vUploadFileTarget.File.
  6. MDriven Turnkey stores the file name in a matching _FileName column when that column exists.
  7. After the upload roundtrip finishes, the component starts the next file.

The sequencing is required. If the component uploaded all files to the same temporary target, later files would overwrite earlier files.

Example model

This example uses the following model structure:

Element Type Purpose
Project Class The ViewModel root. It owns the collection of documents.
Document Class One uploaded document.
Project.Document Association end / collection Holds the Documents belonging to the Project.
Document.File Blob attribute Stores the uploaded file content.
Document.FileName String attribute Stores the uploaded file name.

For example, if a user selects proposal.pdf and budget.docx, the result is two Document objects: one with FileName = proposal.pdf and one with FileName = budget.docx.

Configure the ViewModel

1. Create the root and temporary variable

  1. Create a ViewModel with Project as its root.
  2. Note the ViewModel class ID. The component uses it when it calls the upload client API.
  3. Add a ViewModel variable named vUploadFileTarget with type Document.

vUploadFileTarget is a temporary reference. Before each file upload, it must refer to the new Document that will receive that file.

2. Add hidden upload-target columns

Add these generic ViewModel columns. Set Visible Expression to false for both columns because they are component targets, not user-facing fields.

ViewModel column name Value expression Purpose
AttachmentUploadTarget vUploadFileTarget.File The Blob target used by the component upload call.
AttachmentUploadTarget_FileName vUploadFileTarget.FileName Receives the uploaded file name.

The _FileName suffix is significant. When MDriven Turnkey uploads to AttachmentUploadTarget, it looks for a column at the same ViewModel nesting named AttachmentUploadTarget_FileName. If it exists, Turnkey assigns the uploaded file name to it. Without this column, the file content can be stored but the file name is not retained by this mechanism.

3. Add the visible upload column

  1. Add another generic ViewModel column. This is the visible upload button; its column name is your choice.
  2. Select Content override for the column.
  3. Open TV and Attributes.
  4. Add the tagged value Angular_Ext_Component.
  5. Set its value to the component name. This example uses multifiles.

The tagged-value value must match the component directory name and component file names. For this example, all three use multifiles:

  • Tagged value: Angular_Ext_Component=multifiles
  • Directory: EXT_Components/multifiles
  • Files: multifiles.cshtml and multifiles.js

4. Add the action that creates each Document

Create a ViewModel action named AddDocumentToProject. The JavaScript below calls this exact action name.

Set the action's Execute expression to:

let x = Document.Create in (
  vCurrent_Project.Document.Add(x);
  vUploadFileTarget := x;
  x
)

Replace the example names if your model differs:

Example name Replace with
Document Your class that owns the Blob and file-name attributes.
vCurrent_Project The ViewModel reference to the current root object.
Project Your root class.
Document after vCurrent_Project. The association end that contains the created objects.
vUploadFileTarget Your temporary variable name, if you chose a different name.

The action must both add the new object to the Project and assign that object to the temporary variable. Creating the object without changing vUploadFileTarget leaves the next upload pointed at the wrong target.

Add the EXT Component files

1. Create the component directory

In the model assets directory, which is named similarly to xyz123_MDrivenServerA0_AssetsTK, create this directory structure:

EXT_Components/
  multifiles/
    multifiles.cshtml
    multifiles.js

Create EXT_Components if it does not already exist. When you manage Turnkey site files through FTP, see HowTos:Access Your Turnkey Site with FTP.

2. Add the component markup

Add the following to multifiles.cshtml.

Replace Project_UploadMultipleFilesButton with <ViewModelName>_<UploadColumnName>. This CSS class identifies the ViewModel and the content-override column that use the component. The original example description refers to this format; use the actual names from your ViewModel consistently.

Change the accept attribute to the file extensions your application allows. The example allows PDF, Microsoft Word, and OpenDocument text files. The multiple attribute enables selection of more than one file.

<div class="Project_UploadMultipleFilesButton
            tk-component ctButton tk-button NoLabel"
     multifiles>
  <div class="tk-file-upload__inner">
    <input type="file"
           id="addAttachments"
           class="tk-file-upload__native"
           accept=".pdf,.docx,.doc,.odt"
           readmultifiles
           multiple />
    <label for="addAttachments"
           title="[ViewModelColumnLabel]"
           class="tk-file-upload__interactive ripple-effect">
      [ViewModelColumnLabel]
      <div class="tk-file-upload__progress"
           style="width: {{ViewModelRoot.ViewData['FileUploadPercent__undefined.AttachmentUploadTarget']}}%;"></div>
    </label>
  </div>
  <span class="tk-file-upload__name">{{ViewModelRoot.ViewData['FileUploadName__undefined.AttachmentUploadTarget']}}</span>
</div>

The progress and name bindings use AttachmentUploadTarget. If you renamed that hidden Blob column, change both bindings to the new column name.

3. Add the upload sequencing logic

The supplied component logic uses ExecuteAfterFullRoundtrip around each server operation. It waits for the action that creates the next Document to finish before it calls UploadFile, and waits for that upload before it starts the next file.

The following excerpt contains the upload sequence and the beginning of the readmultifiles directive from the original example. Update every example-specific name before use:

  • AddDocumentToProject: the ViewModel action that creates the next target object.
  • AttachmentUploadTarget: the hidden Blob ViewModel column.
  • multifiles: the component and directive name.
function UploadFiles(files, scope, index = 0) {
    const file = files[index];

    scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitForReadyToExecute' + index, null, () => {
        scope.data.Execute('AddDocumentToProject');

        scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitForSkapaBilagaToFinish' + index, null, () => {
            scope.StreamingViewModelClient.UploadFile(file.Target, file.VMClassId, file.Data, file.FileName);

            scope.StreamingViewModelClient.ExecuteAfterFullRoundtrip('waitingForUploadFileToFinish' + index, null, () => {
                if (index < files.length - 1) {
                    UploadFiles(files, scope, index + 1);
                }
            });
        });
    });
}

function InstallTheDirectiveFor_multifiles(streamingAppController) {
    streamingAppController.directive('readmultifiles', ['$document', function ($document) {
        return {
            link: function (scope, element, attr) {
                element.bind('change', function () {
                    const maxSizeInBytes = 100000000;
                    var nrExpectedFiles = element[0].files.length;
                    var filesToUpload = [];
                    angular.forEach(element[0].files, function (item) {
                        if (maxSizeInBytes === 0 || item.size <= maxSizeInBytes) {
                            const filereader = new FileReader();
                            const filename = item.name;
                            const vmclassid = scope.data.VMClassId.asString;
                            const viewModelName = scope.data.ViewModelName;
                            const target = viewModelName + '.AttachmentUploadTarget';

                            filereader.onloadend = function (loadendevent) {
                                var data = loadendevent.target.result;
                                var fileToUpload = {
                                    Target: target,
                                    VMClassId: vmclassid,
                                    Data: data,
                                    FileName: filename
                                };
                                filesToUpload.push(fileToUpload);
                                if (filesToUpload.length == nrExpectedFiles) {
                                    UploadFiles(filesToUpload, scope);
                                }
                            };

                            filereader.onabort = function (abortevent) {
                                filesToUpload = [];
                                nrExpectedFiles = -1;
                            };

The available example ends at this point. Do not assume omitted closing code or file-reading calls from this excerpt. Obtain and test the complete component JavaScript for your Turnkey version before publishing it to a production site.

Test the component

  1. Open a Project in the ViewModel that contains the content-override upload column.
  2. Select two or more files that match the accept filter.
  3. Wait for the upload activity to complete. Do not navigate away while uploads are in progress.
  4. Verify that the Project has one new Document per selected file.
  5. Verify that each Document has the expected Blob content and FileName value.
  6. Select another group of files and verify that the new files create new Documents rather than replacing the prior files.

The component markup displays progress inside the upload button and displays the current file name beneath it. Uploaded content still needs an appropriate UI elsewhere if users must see, open, or download it. For download configuration, use HowTos:Using BlobDownloadLink.

Limits and implementation notes

  • The example sets maxSizeInBytes to 100000000. Files larger than that value are excluded by the shown condition. Set it to 0 if the component should not apply a client-side size limit.
  • The browser accept attribute controls the file types presented by the picker. Keep it aligned with the file types your application accepts.
  • The temporary variable retains the last created Document after an upload group finishes. The next upload starts by creating and assigning a new target object.
  • Use the same nesting for the Blob target column and its matching _FileName column.
  • Component names, directory names, file names, Angular directive names, and the Angular_Ext_Component tagged value are all coordinated identifiers. A mismatch prevents the component from loading or binding as intended.

See also