As we all know by now, Power Apps Portals profile pictures are not yet an OOB feature. In the past, I have followed custom solutions posted by others that were very JavaScript heavy and not all that clean. Additionally, when these solutions are implemented in a portal used by thousands of people, the massive amounts of web files that get created drastically slow down portal speed as it tries to pre-load all of these graphics (whose parent pages were set to 'Home'). Additionally, these web files were not associated with the actual portal contact records. Lastly, when a user uploaded a new image, they would not see it on their profile until the next time the portal automatically refreshed its cache.
So I implemented a cleaner solution (in my opinion). I created a new entity form that used the OOB 'Profile Web Form' and set up note attachments (along with the necessary entity permissions for contact(self) and notes(parent)). I then created a custom web template and copied the code from the 'Page with Side Navigation (2 columns)' web template into it.
I then added another block to the template and included a line for my custom profile entity form:
{% block rightside %}
{% entityform name:'Profile' %}
{% endblock %}
Then I created another custom web template for retrieving the most recently uploaded jpeg attachment (but you can add in the ability for other image types if you need) associated to the logged in contact's record:
{% fetchxml profilepicfetchxml %}
<fetch version='1.0' output-format='xml-platform' mapping='logical'>
<entity name="contact">
<attribute name="contactid" />
<filter type="and">
<condition attribute="contactid" operator="eq" value="{{ user.id }}" />
</filter>
<link-entity name="annotation" from="objectid" to="contactid" link-type="inner" alias="note">
<attribute name='filename' />
<attribute name='notetext' />
<attribute name='annotationid' />
<attribute name='documentbody' />
<order attribute="createdon" descending="true" />
<filter type='or' >
<condition attribute='mimetype' operator='eq' value='image/jpeg' />
</filter>
</link-entity>
</entity>
</fetch>
{% endfetchxml %}
I added an include reference for this in the profile web template, and then added a new div to display the top result's document body as a base64 jpeg. So this is what the finished web template looks like:
{% extends "layout_2_column_wide_right" %}
{% block aside %}
{% include "anc_profile_picture_fetch" %}
<div style="position:relative">
<div id="profile-pic" onclick="$('#AttachFile').click();$('#confirm-upload').show();$('#pic').css('opacity',.3);" style="cursor:pointer;">
{% if profilepicfetchxml.results.entities.size > 0 %}
<img src="data:image/jpeg;base64,{{ profilepicfetchxml.results.entities[0]['note.documentbody'] }}" style="width: 100%; height: auto;" id="pic">
{% else %}
<img src="~/profile/default-profile.jpg" style="width: 100%; height: auto;" id="pic">
{% endif %}
</div>
<button class="btn btn-success" type="button" onclick="$('#UpdateButton').click();" id="confirm-upload" style="display:none;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);">Confirm Upload</button>
</div>
<div class="hidden-xs hidden-print">
{% include "side_navigation" depth_offset: 1 %}
</div>
{% endblock %}
{% block rightside %}
{% entityform name:'Profile' %}
{% endblock %}
You will notice I also have a default picture (saved as a web file) that displays if no results are found through fetch. I also added a custom upload confirmation button that appears over the profile picture when it is clicked. This button simply triggers a click event on the profile form's submit button. On the Profile web page, I added a CSS rule to hide the attach file option so users only click on the image to open their file explorer (as well as some styling for the navigation pane):
.file-cell {
display: none !important;
}
.side-nav li ul li {
position: relative;
display: block;
padding: .5rem 1rem;
text-decoration: none;
background-color: #fff;
border: 1px solid rgba(0,0,0,.125);
}
Lastly, to allow users to preview their image selection, I added the following Javascript:
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
$('#pic').attr('src', e.target.result);
}
reader.readAsDataURL(input.files[0]); // convert to base64 string
}
}
$("#AttachFile").change(function() {
readURL(this);
});
Overall, this solution works much better than ones we have seen/used in the past and avoids the issue of slower portal load times (changes to profile pictures are also instant with this solution).
Here is what our custom profile page looks like with the default image:

Here is a plugin I also wrote to automatically set the contact record's entity image to the uploaded picture from portal (on create of annotation):
using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Query;
namespace CustomPlugins
{
public class SetProfilePicture : Plugin
{
private readonly string postImageAlias = "postImage";
public SetProfilePicture() : base(typeof(SetProfilePicture))
{
base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(40, "Create", "annotation", new Action<LocalPluginContext>(ExecuteSetProfilePicture)));
}
protected void ExecuteSetProfilePicture(LocalPluginContext localContext)
{
if (localContext == null)
{
throw new ArgumentNullException("localContext");
}
IPluginExecutionContext context = localContext.PluginExecutionContext;
IOrganizationService orgService = localContext.OrganizationService;
ITracingService tracingService = localContext.TracingService;
Entity postImageEntity = (context.PostEntityImages != null && context.PostEntityImages.Contains(this.postImageAlias)) ? context.PostEntityImages[this.postImageAlias] : null;
string documentbody = postImageEntity.Attributes.Contains("documentbody") ? postImageEntity.Attributes["documentbody"].ToString() : "";
if ((context.Depth <= 1) && context.InputParameters.Contains("Target") && context.InputParameters["Target"] is Entity)
{
Entity note = (Entity)context.InputParameters["Target"];
string fetch = String.Format(@"
<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
<entity name='annotation'>
<attribute name='annotationid' />
<filter type='and'>
<condition attribute='annotationid' operator='eq' value='{0}' />
</filter>
<link-entity name='contact' from='contactid' to='objectid' link-type='inner' alias='contact'>
<attribute name='contactid' />
</link-entity>
</entity>
</fetch>", note.Id);
EntityCollection result = orgService.RetrieveMultiple(new FetchExpression(fetch));
if (result.Entities.Count > 0)
{
Entity contact = new Entity("contact", Guid.Parse(result.Entities[0].GetAttributeValue<AliasedValue>("contact.contactid").Value.ToString()));
contact["entityimage"] = Convert.FromBase64String(documentbody);
orgService.Update(contact);
}
}
}
}
}
Please feel free to comment with any suggestions/feedback.