Power Pages

Upload to a File column on record creation using the Portal Web API

Here’s a sample of the end result:

Why do this?

You may be asking why upload using the API when we can now use a basic form or multistep form to upload to file columns?
One reason is with forms, it’s only only possible in Edit mode, meaning the record must already exists. This can make it a more clunky, click-heavy process than it needs to be.
Plus, the web API is all about opening up new UI / UX possibilities and breaking free from forms. Once you’ve mastered these techniques, the user interface / user experience is totally up to you. Maybe you want a drag and drop experience that creates child records… Maybe you want to use them in a PCF control… so many possibilities 😎

Note – In case you’re impatient like me, there’s a full web template at the end of this post that includes uploading new files, retrieving them and updating the page content to display them (in this case, setting the source of a HTML audio player with the uploaded MP3 file), providing visual feedback… and error handling. You’re welcome 😊

Introduction

First of all, a massive thank you to the awesome, the talented, the selfless Guido Preite, creator of the incredible Dataverse REST Builder application for XRMToolbox. If you use the Portal Web API (or the Dataverse Web API) for that matter, you really should check out his tool. It’s such an epic timesaver!… 😎
As if creating an amazing tool wasn’t enough, Guido went out of his way to help me in my exploration around uploading to Image and File columns in the Portal Web API even before his tool specifically supported this in the world of portals 🙏 🙇‍♂️

If you’re already familiar with uploading images to the Portal Web API (see my post here), you’ll find many similarities here. The key difference is that with images, we read and transferred the content in base64 format whereas for File columns, we’ll read and transfer the file contents as a byte array.

Here are the high-level steps involved with uploading to a File column to a new record:

  • Authenticate with the Portal Web API
  • Provide a form input to select a file
  • Get the name of the selected file
  • Get the contents of the selected file (in binary format) using the readAsArrayBuffer API
  • Create the record using the portal web API
  • Upload the file to the newly created record using Portal Web API

Prerequisites

  • Create a File column in your Dynamics instance / Model Driven App
  • Set up the necessary Table Permissions and Site Settings to allow use of the Portal Web API with the required entity and File column (I use another awesome XRMToolBox application for this, Power Portal Web API Helper by the ever helpful and super knowledgeable Omar Zaarour)
  • Install XRMToolBox
  • Install the Dataverse REST Builder tool
  • Connect to your environment

Step 1 – Add a form allowing the user to select a file

My form contains a text input for the track name and a file input that accepts a single MP3 file.
Planning ahead, I’ve added a hidden div and an audio player so that the user can preview their audio upon successful upload

<div id="upload-Audio">
    <h1>Create Track</h1>
    <form id="track-content">
        <label for="track-name">Track Name</label>
        <input id="track-name" type="text">
        <input id="file-upload" accept="audio/mpeg,audio/mpeg3,audio/x-mpeg-3,audio/ogg" type="file">
        <a class="btn btn-default" id="submitFile" style="display:none">Upload</a>
    </form>
    <!-- This div and audio player is hidden by default but will be shown if the upload succeeds -->
    <div class="showOnSuccess" role="alert" style="display:none; margin-top: 1em;">
        <audio id="upload-player" controls>
            <source type="audio/mpeg">Your browser does not support the audio element.
        </audio>
    </div>
</div>

Step 2 – Listen out for a file selection and perform some checks

First we need an event listener. In this example, I’ll listen for a change in the file upload field.
I wouldn’t want to upload a track without a name, nor to allow upload of the wrong file type (for security reasons as well as usability)… so I’ve added checks to ensure that these are populated.

function createFile() {
    var record = {};
    // Get the name from the form field and escape any special characters
    record.musdyn_name = encodeURIComponent(trackName);
    $('#upload-Audio h1').text('Uploading...');
        webapi.safeAjax({
            type: "POST",
            url: "/_api/musdyn_tracks",
            contentType:"application/json",
            data: JSON.stringify(record),
            success: function (data, textStatus, xhr) {
                // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse
                var entityID = xhr.getResponseHeader("entityid");
                // Provide some visual feedback re success of this stage
                $('#upload-Audio h1').text('Track created, uploading audio file');
                // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record
                getFileContentsAndMetadata(entityID);
            },
            error: function (xhr, textStatus, errorThrown) {
                // If the creation fails, provide some visual feedback
                $('#upload-Audio h1').text('Uh oh, error creating the track');
            }
        });
}

Step 3 – Create the Track record using the portal Web API

Note – before you make any API calls, you’ll need to include the API Wrapper AJAX function to verify the source. Don’t worry, this is as easy as copying a snippet. Follow my quick guide here

Generating our API call using Dataverse REST Builder

Launch XRMToolBox and connect to your environment
Open Dataverse REST Builder from the list of tools
Go to File > New Collection
Click on New Request
Set the Request Type to Manage File Data
In the Configure tab, set the following options as required:

Screenshot of how to create a record with the portal Web API, using Dataverse REST Builder in XRMToolbox
Open the Portals tab. There’s our code, helpfully written for us 🙂

Dataverse REST Builder - Portals tab - portal Web API call to create a Track record

All the heavy listing here is done by Dataverse REST Builder. All we need to do is:

  • Wrap Guido’s code in a function so it can be called on demand
  • Replace the hard-coded name with our variable
  • Decide what we want to do on success completion, and on error

Here’s the format of a function:

function nameOfFunction() {
	// The code goes here
}

We’ll be ‘chaining’ a number of functions together. If the first succeeds, take some information and fire the next. If that succeeds, take some information and fire the next
In this example, that’ll be:

  1. Run the createRecord function
  2. If that succeeds, run the getFileContentsAndMetadata function, passing it the GUID of our newly created record
  3. If that succeeds, run the uploadFile function, passing it the file contents, metadata and our GUID

…plus we’ll be providing visual feedback along the way about progress / success / any errors encountered

function createFile() {
    var record = {};
    // Get the name from the form field and escape any special characters
    record.musdyn_name = encodeURIComponent(trackName);
    $('#upload-Audio h1').text('Uploading...');
        webapi.safeAjax({
            type: "POST",
            url: "/_api/musdyn_tracks",
            contentType:"application/json",
            data: JSON.stringify(record),
            success: function (data, textStatus, xhr) {
                // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse
                var entityID = xhr.getResponseHeader("entityid");
                // Provide some visual feedback re success of this stage
                $('#upload-Audio h1').text('Track created, uploading audio file');
                // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record
                getFileContentsAndMetadata(entityID);
            },
            error: function (xhr, textStatus, errorThrown) {
                // If the creation fails, provide some visual feedback
                $('#upload-Audio h1').text('Uh oh, error creating the track');
            }
        });
}

 

Step 4 – Get the contents and the name of the selected file

At this stage, although there is a file selected, the contents aren’t yet available in the browser and nothing has been uploaded. We need to tell the browser to read the file and make its contents available before we can send anything with the API.

If there is a filename and a file in the correct format, run the uploadFile function, passing it the file content, file name and the GUID of the newly created record

function getFileContentsAndMetadata(entityID) {
    // Get the name of the selected file
    var fileName = encodeURIComponent(document.getElementById('file-upload').files[0].name);
    // Get the content of the selected file
    var file = document.getElementById('file-upload').files[0];
    // If the user has selected a file
    if (file) {
        // Read the file as a byte array
        var reader = new FileReader();
        reader.readAsArrayBuffer(file);
        // The browser has finished reading the file, we can now do something with it...
        reader.onload = function(e) {
            // The browser has finished reading the file, we can now do something with it...
            var fileContent = e.target.result;
            // Run the function to upload to the Portal Web API, passing the GUID of the newly created record and the file's contents and name as inputs
            uploadFile(fileContent,fileName,entityID);
        };
    }
}

Step 5 – Upload to the Portal Web API

In this example, I use PUT as I only needed to update a single column. Otherwise, we’d use PATCH (whether that’s to create a record or to update multiple columns for an existing record).

Dataverse REST Builder has done all the heavy lifting for us. So let’s add the finishing touches. All we need to do is:

  • Wrap this code in a function, allowing us to call it on demand
  • Swap the hard-coded GUID, file name and file contents with our variables
  • (Optional) Design what to do on success / error (provide some visual feedback at least)

Building the file upload API call using Dataverse REST Builder

Go to File > New Collection

Click on New Request

Set the Request Type to Manage File Data

In the Configure tab, set the following options as required:

Dataverse REST Builder - configuration to upload to a File column

Open the Portals tab

Dataverse REST Builder - Portals tab - portal Web API call to upload to a File column

 

Copy and paste the code into a function called uploadFile

Guido does all kinds of clever stuff for us just in case the file contents aren’t already in a suitable format (e.g. if we needed to convert form base64). Luckily, ours will be in the wight format do most of what we’re doing is nice and straight forward.

We can remove all the file conversion stuff

Now all we need to do is replace the hardcoded file name and file contents with our variables

// Upload the file to the newly created Track record
function uploadFile(fileContent,fileName,entityID) {
    var record = {};
    record.musdyn_name = fileName;

    webapi.safeAjax({
        type: "PUT", // NOTE: right now Portals requires PUT instead of PATCH for the upload
        url: "/_api/musdyn_tracks(" + entityID + ")/musdyn_audiotrack?x-ms-file-name=" + fileName,
        contentType: "application/octet-stream",
        data: fileContent,
        processData: false,
        success: function (data, textStatus, xhr) {
            // Provide some visual feedback re successful upload
            $('#upload-Audio h1').html('<span style="color: #1ed760;" class="glyphicon glyphicon-ok"></span> Here\'s your audio');
            // Set the source of the hidden audio player
            audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID;
            $('#upload-player').attr('src',audioPlayerSrcURL)
            // Show the audio player
            $('.showOnSuccess').slideDown(200);
            // Hide the form
            $('#track-content').slideUp(200);
        },
        error: function (xhr, textStatus, errorThrown) {
            // If error occurs uploading the file, provide visual feedback
            $('#upload-Audio h1').text('Uh oh, uploading file the track');
        }
    });
}

The visual feedback – where does the audio file come from?

In my example, I’m uploading an MP3 file. On success, I figured it would be reassuring to allow the user to play the file they just uploaded.
If the upload fails (the ajax returns an error), I’ll show a basic error message on screen and log a more detailed error in the console.
All we need to access our file is a download URL in the following format. It needs 3 pieces of information from us:

  • The logical name of the table
  • The logical name of the column
  • The GUID of the record

Here’s an example:

https://songify.powerappsportals.com/File/download.aspx?Entity=musdyn_track&Attribute=musdyn_audiotrack&Id=c9013cdf-3d35-eb11-8fed-281878c98667

…and here’s how that looks using my entityID variable (no use hard-coding the GUID!!) and setting the source of the audio player with jQuery:

audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID;
$('#upload-player').attr('src',audioPlayerSrcURL);

The full web template

{% include 'Portal Web API Wrapper' %}
<div id="upload-Audio">
    <h1>Create Track</h1>
    <form id="track-content">
        <label for="track-name">Track Name</label>
        <input id="track-name" type="text">
        <input id="file-upload" accept="audio/mpeg,audio/mpeg3,audio/x-mpeg-3,audio/ogg" type="file">
        <a class="btn btn-default" id="submitFile" style="display:none">Upload</a>
    </form>
    <!-- This div and audio player is hidden by default but will be shown if the upload succeeds -->
    <div class="showOnSuccess" role="alert" style="display:none; margin-top: 1em;">
        <audio id="upload-player" controls>
            <source type="audio/mpeg">Your browser does not support the audio element.
        </audio>
    </div>
</div>

<script>
var trackName;

// on change to the file upload form field, check we have the required inputs and run the createFile function
$( "#file-upload" ).on( "change", function() {
    trackName = document.getElementById('track-name').value;
    //selectedFile = $( "#file-upload" ).val();
    var fileMimeType = document.getElementById('file-upload').files[0].type;
    alert(fileMimeType);
    if(trackName != '') {
    // Check this is an audio file
        if(fileMimeType.startsWith("audio/")) {
            createFile();
        } else {
            alert("This doesn't appear to be an audio file");
        }
    }
});


function createFile() {
    var record = {};
    // Get the name from the form field and escape any special characters
    record.musdyn_name = encodeURIComponent(trackName);
    $('#upload-Audio h1').text('Uploading...');
        webapi.safeAjax({
            type: "POST",
            url: "/_api/musdyn_tracks",
            contentType:"application/json",
            data: JSON.stringify(record),
            success: function (data, textStatus, xhr) {
                // If record creation succeeds, get the GUID of the newly created record and store in a varibale for easy reuse
                var entityID = xhr.getResponseHeader("entityid");
                // Provide some visual feedback re success of this stage
                $('#upload-Audio h1').text('Track created, uploading audio file');
                // Run the getFileContentsAndMetadata function, passing the GUID of the newly created record
                getFileContentsAndMetadata(entityID);
            },
            error: function (xhr, textStatus, errorThrown) {
                // If the creation fails, provide some visual feedback
                $('#upload-Audio h1').text('Uh oh, error creating the track');
            }
        });
}

function getFileContentsAndMetadata(entityID) {
    // Get the name of the selected file
    var fileName = encodeURIComponent(document.getElementById('file-upload').files[0].name);
    // Get the content of the selected file
    var file = document.getElementById('file-upload').files[0];
    // If the user has selected a file
    if (file) {
        // Read the file as a byte array
        var reader = new FileReader();
        reader.readAsArrayBuffer(file);
        // The browser has finished reading the file, we can now do something with it...
        reader.onload = function(e) {
            // The browser has finished reading the file, we can now do something with it...
            var fileContent = e.target.result;
            // Run the function to upload to the Portal Web API, passing the GUID of the newly created record and the file's contents and name as inputs
            uploadFile(fileContent,fileName,entityID);
        };
    }
}

// Upload the file to
function uploadFile(fileContent,fileName,entityID) {
    var record = {};
    record.musdyn_name = fileName;

    webapi.safeAjax({
        type: "PUT", // NOTE: right now Portals requires PUT instead of PATCH for the upload
        url: "/_api/musdyn_tracks(" + entityID + ")/musdyn_audiotrack?x-ms-file-name=" + fileName,
        contentType: "application/octet-stream",
        data: fileContent,
        processData: false,
        success: function (data, textStatus, xhr) {
            // Provide some visual feedback re successful upload
            $('#upload-Audio h1').html('<span style="color: #1ed760;" class="glyphicon glyphicon-ok"></span> Here\'s your audio');
            // Set the source of the hidden audio player
            audioPlayerSrcURL = '/File/download.aspx?Entity=musdyn_track' + '&Attribute=musdyn_audiotrack&Id=' + entityID;
            $('#upload-player').attr('src',audioPlayerSrcURL)
            // Show the audio player
            $('.showOnSuccess').slideDown(200);
            // Hide the form
            $('#track-content').slideUp(200);
        },
        error: function (xhr, textStatus, errorThrown) {
            // If error occurs uploading the file, provide visual feedback
            $('#upload-Audio h1').text('Uh oh, uploading file the track');
        }
    });
}
</script>
Franco Musso

You may also like

Leave a reply

Your email address will not be published.

More in Power Pages