PowerApp Portal: Change your Profile Photo using The Portal WebAPI

Disclaimer: The Web API is still in preview, don’t use in production just yet! More info here.

The New Portal WebAPI opened so many doors to customize the portal outside the normal entity forms, web forms , entity lists etc.

You remember that Profile Photo container in the profile pages on the portal? Almost every client I worked with thought it is a working feature but it is not. We end up hiding this container using CSS to remove that confusion.

With the introduction of WebAPI, now we can do it with a supported way.

On the contact entity, there is a field called entityimage, this field is a byte array that holds the picture of the contact. We have two cases to worry about, on the load of the page, we need to read the image from the logged-in contact record. Also, we need to add an upload button to allow portal users to change their photos. We will end up with something like this:

Before starting, we need to setup the portal for the web api and to expose the contact entity properly.

  1. Make sure you have an entity permission setup that allows reading and updating contact. This should be the default for the logged in user since they can modify their profile data.
  2. To enable the contact entity for WebAPI, you need to create the following site settings:
    • site setting name: webapi/contact/enabled value:true
    • site setting name: webapi/contact/fields value:* (Notice I put * to have all fields but if you want to only access the entityimage field, just list the comma separated logical names of the fields you want exposed). If you have hard time knowing the field name, use Power App Portal Web API Helper plugin on XRM toolbox to enable/disable fields and entities for portal.
  3. Make sure that your portal is 9.2.6.41 or later.

There is a piece of code that you need for all of your web api calls. In the normal situation, I prefer to have this piece of code in a web template or in a web file that gets loaded on the website level. For our scenario I will include this piece of code as part of the solution so don’t worry about copying it now as it will be part of the final code. Basically, this piece of code takes your API Request, adds an authentication header and send and AJAX request to Dataverse, nothing fancy.

(function(webapi, $){
		function safeAjax(ajaxOptions) {
			var deferredAjax = $.Deferred();
	
			shell.getTokenDeferred().done(function (token) {
				// add headers for AJAX
				if (!ajaxOptions.headers) {
					$.extend(ajaxOptions, {
						headers: {
							"__RequestVerificationToken": token
						}
					}); 
				} else {
					ajaxOptions.headers["__RequestVerificationToken"] = token;
				}
				$.ajax(ajaxOptions)
					.done(function(data, textStatus, jqXHR) {
						validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
					}).fail(deferredAjax.reject); //AJAX
			}).fail(function () {
				deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
			});
	
			return deferredAjax.promise();	
		}
		webapi.safeAjax = safeAjax;
	})(window.webapi = window.webapi || {}, jQuery)

Moving on. As we said before, we need to tackle the page load case and the upload button case.

For the page load, we need to query the entityimage using FetchXML, the following script retrieves the field and assign its value to entityImageBytes liquid variable.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}

Next, we need to convert these bytes to a format that the Html “img” understands, in this case, we need to convert it to a base64 string. In the below two java script function, we do exactly that. Thanks to Franco Musso’s blog, where I borrowed part of the logic to do the format conversion that saved me some googling time 🙂

The loadProfileImage function, finds the image tag that resides on the profile page. I had to do some other styling for the div around the image but it is up to you. Then, that byte array we got through FetchXML is converted to a long comma separated string that gets fed to bytesStringToBase64 function that encodes that byte sequence into a base64 format. Once we get that base64 string, we assign it as the “src” attribute of the “img” tag. At that moment, you should have the profile picture populated with an image if it exists.

function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}

Next, we need to add the ability of uploading a photo by the logged-in user. To do that, we will add some HTML to add the upload button and the file selector. On the file input, you notice that we call a function (convertImageFileToBase64String) that converts the file into a base64 string, why is that? because when we update the image file in Dataverse, the JSON payload of the UPDATE request needs the image to be in base64 format.

<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>

The convertImageFileToBase64String function basically reads the file and convert it to a base64 string using the FileReader.readAsDataURL method, the result is stored in image_file_base64 variable, we need that later when we do the UPDATE request. You can be more professional than me and avoid the public variable and make the function return a promise instead of this sketchy method 🙂

var image_file_base64;
function convertImageFileToBase64String(element) {
    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
    }
    reader.readAsDataURL(file);
}

We only have on thing left, the upload button. This button should take the base64 image that we just converted when we uploaded the image and send it to Dataverse using an UPDATE request. the image_file_base64 is already in the format we want, the only thing we need to do is to call our AJAX Wrapper we mentioned in the beginning and provide it with AJAX request options as shown below. Notice that on success of the call, I optimistically update the image without the need to refresh the page.

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
               }
        });
}

Now to the full code and how to use in a two steps in your portal.

 
 <!--Get the user profile photo (entityimage)-->
 {% fetchxml contactImageQuery %}
     <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="contact" >
            <attribute name="entityimage" />
            <attribute name="contactid" />
            <filter type="and">
            <condition attribute="contactid" operator="eq" uitype="contact" value="{{ user.id }}" />
            </filter>
        </entity>
    </fetch>
{% endfetchxml %}

{% assign entityImageBytes = contactImageQuery.results.entities[0].entityimage %}
<div style="display:inline-block;">
    <label>Update your profile photo </label>
    <input type="file" name="file" id="file" onchange="convertImageFileToBase64String(this)"/>
    <div id="uploadPhotoDiv"  style="visibility:hidden">
    <button type="button" onclick="onUploadClicked()" id="uploadPhoto">Upload</button>
    </div>
<div>
<script>
$(document).ready(function() {
    // Load the profile photo from dataverse on document ready
    loadProfileImage();
});

// ajax wrapper provided by Microsoft.
(function(webapi, $){
        function safeAjax(ajaxOptions) {
            var deferredAjax = $.Deferred();
     
            shell.getTokenDeferred().done(function (token) {
                // add headers for AJAX
                if (!ajaxOptions.headers) {
                    $.extend(ajaxOptions, {
                        headers: {
                            "__RequestVerificationToken": token
                        }
                    }); 
                } else {
                    ajaxOptions.headers["__RequestVerificationToken"] = token;
                }
                $.ajax(ajaxOptions)
                    .done(function(data, textStatus, jqXHR) {
                        validateLoginSession(data, textStatus, jqXHR, deferredAjax.resolve);
                    }).fail(deferredAjax.reject); //AJAX
            }).fail(function () {
                deferredAjax.rejectWith(this, arguments); // on token failure pass the token AJAX and args
            });
     
            return deferredAjax.promise();  
        }
        webapi.safeAjax = safeAjax;
    })(window.webapi = window.webapi || {}, jQuery)

function onUploadClicked(){
        $("#uploadPhoto").text("Uploading...");
        webapi.safeAjax({
            type: "PATCH",
            url: "/_api/contacts({{user.id}})",
            contentType: "application/json",
            data: JSON.stringify({
                "entityimage": image_file_base64
            }),
            success: function(res) {
                $("img").first().attr('src', 'data:image/png;base64,' + image_file_base64);
                $("#uploadPhotoDiv").css("visibility","hidden");
                $("#uploadPhoto").text("Upload");
                
            }
        });
}
function loadProfileImage() {
    // Select the profile photo and its container, style them as needed
    var profileImgWell = $($(".well")).css("padding", "1px");
    var profileImg = $($(".well a img"))[0];
    $(profileImg).css("width", "100%");
    $(profileImg).css("height", "100%");

    // get the bytes returned by the fetxhxml query and join them to form a single string
    var bytesData = "{{entityImageBytes| | join: ','}}";

    // convert the byte array to base64 string and assign it to the src attribute of the profile image
    var base64String = bytesData ? bytesStringToBase64(bytesData) : null;
    if (base64String) {
        profileImg.src = 'data:image/jpeg;base64,' + base64String;
    }

}

function bytesStringToBase64(bytesString) {
    if (!bytesString) return null;
    var uarr = new Uint8Array(bytesString.split(',').map(function(x) {
        return parseInt(x);
    }));
    return btoa(String.fromCharCode.apply(null, uarr));
}


var image_file_base64;

function convertImageFileToBase64String(element) {

    var file = element.files[0];
    var reader = new FileReader();
    reader.onloadend = function() {
        image_file_base64 = reader.result;
        image_file_base64 = image_file_base64.substring(image_file_base64.indexOf(',') + 1);
        $("#uploadPhotoDiv").css("visibility","visible");
    }
    reader.readAsDataURL(file);
}

</script>

Let’s assume you pasted the whole code in a web template called “Contact Photo Uploader”, open your profile page or child page on the portal as an administrator, click this little edit icon on the page copy

Click on the HTML source button in the dialog that appears

and paste this line {%include “Contact Photo Uploader”%} and save the html snippet.

That’s it, this will include the whole web template we just built on the page and the result should be something like this without my photo of course :). Note that the upload button will only appear once you select the file.

Power Portal Web API Helper – An XrmToolBox Plugin

Web API for Portals is a new feature where you can finally issue Web API calls from your portal to the CDS. This post doesn’t go in the details of this feature, everything you want to know can be found here. The summary is that when you want to enable an entity for web api on the portal, you need to configure few site settings and some entity permissions and then call the Web API’s using some JavaScript code. This plugin is super simple and it just helps you in enabling/disabling the entity and any attribute for that entity for the WebAPI feature on the Portal. In addition to that, it provides you with simple JavaScript snippets that you can use as a starting point in your project.

How to use this plugin?

  1. Install the plugin from the Tool Library in XrmToolBox.
  2. Open the plugin and connect to your organization. The plugin should look like this

3. Click on Load Entities button from the command bar and the system and custom entities will be listed for you. You can search for your entity name in the filter box. You can also notice that a website selection appears in the command bar, you need to select the target website in case you have more than one in your target organization. Enabling an entity on Website A will only create the proper site settings for Website A, if you change the website, you need to enable the entity again as new site settings need to be created.

Note: Microsoft Docs clearly says that this feature should be used with data entities such as contact, account, case and custom entities. This feature shouldn’t be used on configuration entities such as adx_website or other system entities that store configuration data. Unfortunately and to my knowledge, I couldn’t find a very deterministic way for filtering every entity that shouldn’t be used with the Web API so if you know of a way, please do tell so that I can fix the filtering logic. The current logic is to exclude the list provided by Microsoft which consists mostly of the adx_ entities. I also did some judgment calls on other entities that store configuration data and not business data.

4. Once you select an entity, you can either enable or disable it and you also can select the fields you want to expose. When you are done, just click Save changes from the command bar and all the site settings will be created/updated for you.

5. If you are not very involved in JavaScript, and to help you get started quickly, clicking on Generate Snippets in the command bar will provide you with Create/Update/Delete snippets in addition to the wrapper ajax function that you use to issue the web api calls.

6. As an additional small feature, the JSON object you use for Create and Update snippets will have the selected attributes with default values. If you don’t see some of these attributes in your output JSON string, it is mostly because the attribute is not available for Create or Update as the plugin does the check for those conditions. For example, if you are creating an account record, you can provide the accountid in the create call but not in the update call as the accountid is not updatable.

That’s it, a simple plugin that will save you sometime when looking for those attributes logical names and when you want to enable entities for portal web api feature.

Project Link on GitHub: https://github.com/ZaarourOmar/PowerPortalWebAPIHelper/issues

Business Rules for PowerApps Portals – v1

When it comes to customizing Dynamics 365, I don’t care how we do it, I care about enabling the customers to use the system easily after it gets delivered to them. This of course means if we can get things done by OOB configuration and customization wizards, then it is the way to go, the last option is to write code. One example is the use of Business Rules instead of client side scripting, for simple to medium needs, a business rule can save us (and the customer) from nasty JavaScript code and enable them to change it later without worry.

The same problem applies to the Portals side of Dynamics. I’ve never worked on a portal project where the OOB features satisfy the client needs. This means any small change like hiding a field or a section needs to be backed up by some Javascript that lives inside the Entity form or the Web Form Step. Even though the needed Javascript can be simple, not everyone is comfortable doing it specially if the Dynamics Admin is not a technical person and honestly, they don’t need to know Javascript.

I though of a configuration-based solution that I call Portal Business Rules. This solution doesn’t have a fancy designer like the Business Rules in Dynamics Forms, but it is configuration based and it is capable of producing/modifying Javascript without the need to write it yourself. This solution has many of the common functionalities that a project needs. That being said, and similar to how client side scripting is still needed on the Dynamics side even with the existence of Business Rules, complex needs will still require Javascript on the portal and the good news is that this complex Java script can coexist with my proposed solution.

The current functionality of the solution is limited to:

  1. Each rule is governed by a single IF/ELSE condition.
  2. The rule works with Entity forms and Web form steps.
  3. Each rule can have unlimited number of actions. Actions include Show/Hide fields. Disable/Enable Fields, Make fields Required/Not Required, Set Field Value, Prevent Past Date and Prevent Future Date (for Datetime fields), Show/Hide Sections, Show/Hide Tabs.
  4. A rule will parse the XML of the related form or tab and suggest the fields/sections/tabs to be used in the rule logic.
  5. For some of the field types (Option sets and two option sets), a suggested value table shows up for ease of use. So instead of figuring out the integer value of an option set field, they will be listed for the user to select from.
  6. The ability to use “In” and “Not In” Operators. For example you can say if an option set value is in “2^3^4” which means if the option set is either of these 3 values, then the condition will hold true.
  7. You can see the generated Java script directly in a special tab.
  8. The Generated Java script for all the rules gets injected into the Entity form or web form step Custom Java script field and it is decorated with special comments to make it clear that this is generated by the solution and not by hand.
  9. When a rule is deleted or drafted, its logic gets removed automatically from the corresponding entity form or web form step.
  10. Basic error handling is added so that when the operands has the wrong value format, an error will show up to tell the user to fix it.

Here is a quick video showing the installation steps:

Here is a simple rule creation demo that shows/hides a tab based on a two option set value:

Another demo of multi action rule, where the Job Title field is shown and becomes required if the Company Name field is populated:

Another demo of how an option set is used in a rule. How error handling works if the operand value is of wrong format.

And finally, the “In” Operator is one of the advanced operators. Here is an example of how we can populate a field if the condition falls into one of a predetermined list of values:

Of course, there are many other possible operations features that you want to check out if you install the solution. Manipulating section visibility, field states (enabled and disabled) and many more.

Many will notice that we can only have one condition in a single rule for now and I’m currently thinking on the best way to associate other conditions to a rule with either AND or OR logical operators between them, similar to how Dynamics 365 Business Rules behave.

To be fair, the best solution for this problem is not my proposed solution but is to make the Business rules that currently exist for Dynamics forms work on the Portal Forms as well, I can say that this solution needs to be done by Microsoft itself as there no much visibility on the Business Rules engine for us,developers. Based on my knowledge, the business rules in Dynamics seem to be built using the Windows Workflow Foundation (from looking at their XAML).

In summary, the problem I’m trying to solve is reducing the need for code further, similar to how Business Rules reduced the need for client side scripting on the Dynamics 365 side. If code is still needed, then my solution and custom code can still live together.

Please refer to my repository on Github for installation steps. Feedback is really appreciated.

NOTE: For the Java script functions that I call in the back-end, I use this existing library on GitHub developed by Aung Khaing .

Update October 16, 2019

During some search, I found out that a company called North52 has a similar solution that was done before and they inject Javascript the same way I do but of course with a nicer interface :). I have a bit more functionality provided. Here is the Link