Posted in Entity List, JavaScript/jQuery Plugins, Power Apps Portals

Power Apps Portals – JavaScript Tip #03 – Load Edit modal after record creation

Hi

A very common business requirement I see is to create a record as well as related (1:N) records within the same page. By design, in Power Apps Portals (as well as Dynamics 365 / CDS / Dataverse) we need first to create the main/parent record to be able to create any related records. There are a few different ways to implement this, for example:

  • Web Forms (or Entity Forms with redirect) – having the initial step to the main record creation, and then moving to another step/page with the subgrid
  • Web API – the new Web API allows us to create multiple records in the same request, but requires a good bit of custom development

The above might be enough for most scenarios, but I want to explore here a different one that is very common. Let’s say we have an Entity List (or subgrid) with both Create and Edit actions enabled using an Entity Form modal instead of opening a Web Page – how can we fix this problem then?

We can achieve this requirement by adding a JavaScript on the Entity List (or Entity Form containing the subgrid if that’s your case). The idea of the JavaScript code is to check if there is any created record on every time the list is loaded, and force the Edit Entity Form to open, this will allow the user to enter the related records without having to click on the record. Let’s take a look at how this will look to the end-user:

Technical Implementation

I will explain here, at a high-level, my implementation:

  • I have two global variables to help in my code
    • existingRecordList – this is an array that I will store the existing list items whenever loaded
    • firstLoad – this is a simple variable to control when the page is being loaded for the first time
  • Then I inject a function (CheckNewRecord) to the onload event of my Entity List
  • These are the steps of my main function (CheckNewRecord):
    • Initiate the existingRecordsList in case this is the first load
    • Compare the size between existingRecordsList and Entity List records
    • I am creating a new temporary array to avoid conflicting with my global array
    • For each item of my Entity List:
      • Add the record ID to the temporary array
      • Ignore if this is the first time loading or if we already found which one is the new record
      • Check if the ID doesn’t exist in the global array
      • If it doesn’t exist, it means we found the new record, so we can store that in a separate variable (newRecordId) as well as marking that we already found the new record
    • Reload the global array with the temporary array; this is necessary for the next loads of the Entity List
    • If we found a new record, we need then to open the Edit modal/Entity Form
      • Via jQuery I am finding the element with the ID of the new record, and triggering the click event for the “details-link” class
    • Finally set the firstLoad control variable to false
var existingRecordsList;
var firstLoad = true;

$(document).ready(function() {

    var list = $(".entitylist.entity-grid").eq(0);
    list.on("loaded", CheckNewRecord);
});

function CheckNewRecord() {

    var list = $(".entitylist.entity-grid").eq(0);
    var newRecordFound = false;
    var newRecordId;

    // first load
    if (firstLoad) {
        existingRecordsList = [];
    }

    if (existingRecordsList.length != $(list).find('table tbody tr').length) {

        var tempRecordList = [];

        $(list).find('table tbody tr').each(function() {
            var id = $(this).attr("data-id");
            tempRecordList.push(id);

            if (!firstLoad && !newRecordFound && existingRecordsList.indexOf(id) < 0) {
                newRecordId = id;
                newRecordFound = true;
            }
        });
        // reload global variable with items in temp list
        existingRecordsList = tempRecordList;

        if (newRecordFound && !!newRecordId) {
            // open edit modal
            var newRecordElement = $(list).find("[data-id='" + newRecordId + "']");
            $(newRecordElement).find("a.details-link").eq(0).trigger("click");
        }
    }
    firstLoad = false;
}

Conclusion

The idea of this post was more to show tricks that we can do with an Entity List/Subgrid by applying JavaScript code. This is not limited to opening the Edit Entity Form after the creation, but really anything you might need to perform via JavaScript once a record is created and you don’t want to re-load the page.

This works pretty well with an Entity List/Subgrid containing a small number of records. But be mindful that in case of a large list, this might not work as the new record might not be in the current page.

Posted in Power Apps Portals, Web API

Power Apps Portals – Going beyond with Portals using Web API

Hi

Recently I’ve made a presentation at the Dublin user group on the new Power Apps Portals Web API. In this post I would like to share the slide deck and code used.

I haven’t done any post on the Web API, and the main reason is that I find Microsoft documentation pretty solid on this topic (and I am not a big fan of Microsoft’s documentation for new products).

You can check the official documentation on the Web API here: https://docs.microsoft.com/en-us/powerapps/maker/portals/web-api-overview

Why do we need a Web API?

Currently in Power Apps Portals, we can retrieve CDS data programmatically by using oData or FetchXML, but we have no way to perform insert/update operations against CDS data. The only ways to achieve these operations are through Entity Forms / Web Forms (there are alternative techniques but I won’t go into detail on these).

I have seen several business requirements that would need to perform operations without having an Entity Form associated, let me give you a few examples:

  • Editable Grid
  • Bulk delete within an Entity List
  • Bulk workflow trigger within an Entity List
  • Mark a Case or Notification record as read
  • Insert/Update records on a page being rendered via Liquid Template

Basically with the new Power Apps Portals Web API all those requirements will now be possible to achieve. Basically the Web API will open the doors for many possibilities.

The editable grid

During my presentation, I created an editable grid to be used against the Account entity, let’s take a look at how it looks:

I want to share here the code used in my presentation, but note that there might be a few requirements necessary that I won’t be exploring in this post. At a high-level, this is what is required:

  • Enable Web API via Site Settings (for both Account and Contact entities);
  • Create Entity Permissions (for both Account and Contact entities);
  • Create Web Template with Web API wrapper provided by Microsoft;
  • I have a custom CSS loaded with very simple definitions just to render my custom grid;
  • I have a “loading” gif just to give a better UX while the operation is happening

Okay, now I can talk about the code responsible for rendering my editable grid, as well as perform the Web API operations:

  • I am extending the OOB Layout 1 Column, and overriding the block main, this is just for rendering my content;
  • Now I have a fetch for the Account entity, which I am retrieving the Account Name, Primary Contact and telephone number;
  • I am adding a button responsible for the “New” account – you can see that I am assigning the NewRecord function to the button (we’ll go through that later);
  • Now I start rendering the table representing the editable grid itself, and just to simplify things, I am assigning an ID to each individual cell using the format (<account guid><_attribute name>). This will make it easier to retrieve the values later on, but you could achieve this in other ways as well;
  • For each row, I am adding a “Remove” and “Update” button, assigning a JavaScript function as well

Now we can talk about the JavaScript functions:

  • NewRecord() – This function creates a new line in the table, assigning a new GUID for the (to be created) Account. Here the remove button calls a different function, that won’t be calling the API, only removing the record from the table, and finally the button for create, which will obviously create the Account in the CDS;
  • NewGuid() – This function simply generates a new GUID;
  • CreateRecord(accountId) – This function is responsible for creating the Account and, along with it, it also creates a new Contact record in the same API request call. Finally, it refreshes the page so the remove and update functions are loaded correctly;
  • UpdateRecord(accountId) – This function updates only the Account Name and Telephone, I am not too bothered with the Contact for the sake of this demo;
  • RemoveUnsavedRecord(accountId) – This just removes a row without going through the API;
  • RemoveRecord(accountId) – This function calls the DELETE API method and removes the row from the table
{% extends "Layout 1 Column" %}
{% block main %}

{% include "OR Web API Wrapper Web Template" %}

{% fetchxml accountFetch %}
    <fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
        <entity name="account">
            <attribute name="name" />
            <attribute name="primarycontactid" />
            <attribute name="telephone1" />
            <attribute name="accountid" />
            <order descending="false" attribute="name"/>
        </entity>
    </fetch>
{% endfetchxml %}

    <div>
      <button type="button" class="btn btn-primary new-record btn-sm my-0 glyphicon glyphicon-plus" onclick="NewRecord()"></button>
      <table id="accountTable" class="table table-responsive-md table-striped text-center">
        <thead>
          <tr>
            <th class="text-center hidden">Account Id</th>
            <th class="text-center">Name</th>
            <th class="text-center">Primary Contact</th>
            <th class="text-center">Telephone</th>
            <th class="text-center"></th>
          </tr>
        </thead>
        <tbody>
        {% for acc in accountFetch.results.entities %}
            {% assign accountId = acc.accountid %}
          <tr id={{ accountID }}>
            <td class="hidden" contenteditable="true">{{ accountId }}</td>
            <td contenteditable="true" id="{{ accountID }}_name">{{ acc.name }}</td>
            <td contenteditable="true" id="{{ accountID }}_contactName">{{ acc.primarycontactid.Name }}</td>
            <td contenteditable="true" id="{{ accountID }}_telephone">{{ acc.telephone1 }}</td>
            <td>
              <button type="button" class="btn btn-danger btn-sm my-0 glyphicon glyphicon-remove" onclick="RemoveRecord('{{ accountId }}')"></button>
              <button type="button" class="btn btn-success btn-sm my-0 glyphicon glyphicon-ok" onclick="UpdateRecord('{{ accountId }}')"></button>
            </td>
          </tr>
        {% endfor %}
        </tbody>
      </table>
    </div>

<script>

function NewRecord(){

    var guid = NewGuid();
    var newLine = '';
    newLine += '<tr id=' + guid + '>';
    newLine += '    <td class="hidden" contenteditable="true">' + guid + '</td>';
    newLine += '    <td contenteditable="true" id="' + guid + '_name"></td>';
    newLine += '    <td contenteditable="true" id="' + guid + '_contactName"></td>';
    newLine += '    <td contenteditable="true" id="' + guid + '_telephone"></td>';
    newLine += '    <td>';
    newLine += '        <button type="button" class="btn btn-danger btn-sm my-0 glyphicon glyphicon-remove" onclick="RemoveUnsavedRecord(\'' + guid + '\')"></button>';
    newLine += '        <button type="button" class="btn btn-success btn-sm my-0 glyphicon glyphicon-ok" onclick="CreateRecord(\'' + guid + '\')"></button>';
    newLine += '    </td>';
    newLine += '</tr>';

   $("#accountTable").append(newLine);
   $("#" + guid + "_name").focus();
};

function NewGuid(){
    var dt = new Date().getTime();
    var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = (dt + Math.random()*16)%16 | 0;
        dt = Math.floor(dt/16);
        return (c=='x' ? r :(r&0x3|0x8)).toString(16);
    });
    return uuid;
}

function CreateRecord(accountId){
    
    $("#loading").fadeIn();

    var accountName = $("#" + accountId + "_name").text();
    var telephone = $("#" + accountId + "_telephone").text();
    var contactName = $("#" + accountId + "_contactName").text();
    var contactFirstName = "";
    var contactLastName = "";

    if(!!contactName){
        contactFirstName = contactName.split(" ")[0];
        
        if(contactName.split(" ").length > 1)
            contactLastName = contactName.substr(contactName.indexOf(" ") + 1);
    }

    webapi.safeAjax({
		type: "POST",
		url: "/_api/accounts",
		contentType: "application/json",
		data: JSON.stringify({
			"accountid" : accountId,
            "name": accountName,
            "telephone1" : telephone,
            "primarycontactid":
            {
                "firstname": contactFirstName,
                "lastname": contactLastName
            }
        }),
		success: function (res, status, xhr) {
            $("#loading").fadeOut();
            alert("The record has been created successfully.");
            location.reload();
		},
        error: function(){
            $("#loading").fadeOut();
        }
	});
};

function UpdateRecord(accountId){

    $("#loading").fadeIn();

    var accountName = $("#" + accountId + "_name").text();
    var telephone = $("#" + accountId + "_telephone").text();

    webapi.safeAjax({
        type: "PATCH",
        url: "/_api/accounts(" + accountId + ")",
        contentType: "application/json",
        data: JSON.stringify({
            "name": accountName,
            "telephone1" : telephone
        }),
        success: function (res) {
            $("#loading").fadeOut();
            alert("The record has been updated successfully.");
        },
        error: function(){
            $("#loading").fadeOut();
        }
    });
};

function RemoveUnsavedRecord(accountId){
    
    if(!!accountId){
        $("#"+ accountId).remove();
    }    
};

function RemoveRecord(accountId){
    
    $("#loading").fadeIn();
    if(!!accountId){
        webapi.safeAjax({
            type: "DELETE",
            url: "/_api/accounts(" + accountId + ")",
            contentType: "application/json",
            success: function (res) {
                $("#"+ accountId).remove();
                $("#loading").fadeOut();
                alert("The record has been deleted.");
            },
            error: function(){
                $("#loading").fadeOut();
            }
        });
    }    
};

</script>

{% endblock %}

Can I use the Web API in production?

The Web API is currently in preview, and the general message is that it shouldn’t be used in Production environments. In my personal opinion, this feature is in good shape and shouldn’t have major changes until it reaches General Availability. I also doubt that Microsoft would cancel the rollout for this.

In other words, I would use it in Production, just keep in mind that you may have to re-factor some code in case of any changes from Microsoft.

Here is the slide deck I used in my presentation:

Posted in 365 Saturday, Authentication, Power Apps Portals

365 Saturday – Power Apps Portals Authentication

Hi

A couple of weeks ago we had the first 365 Saturday dedicated only to Power Apps Portals. I would just like say a big thanks to everyone that joined and all organizers and speakers for making it happen.

I presented a session on Authentication for Power Apps Portals and just wanted to share the slide deck (even though it was very simple, the main focus was the demo).

If you missed the online event, you can check the video here:

All event videos are available at the channel: https://www.youtube.com/channel/UChbRRAHWO9OBwmdXsI5kfAw

Posted in JavaScript/jQuery Plugins, Power Apps Portals

Power Apps Portals – JavaScript Tip #02 – Set Attributes Read-Only

Hello and welcome to another JavaScript tip for Power Apps Portals.

In this post I will show you how to set attributes read-only in your Entity Form / Web Form.

A generic way to set an attribute as read-only would be the following:

$("#<attribute name>").attr("readonly", true);

But depending on the datatype of your attribute, some additional changes to the HTML elements might be required. For example, a lookup field is normally rendered with a search button, or a datetime field has the datetime picker next to the input control:

Instead of re-writing the same code every time, let’s create some generic functions passing the field name as parameter, as well as a true/false flag to define if the field should be read-only. We will create a few different functions, according to the field datatype:

Set DateTime ReadOnly

SetDateTimeFieldReadOnly = function (fieldName, readOnly) {
    if (readOnly) {
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").attr("readonly", true);
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").css("cursor", "not-allowed");
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").on("mousedown", function (e) { e.preventDefault(); return false; });
    } else {
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").attr("readonly", false);
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").css("cursor", "default");
        $('#' + fieldName).siblings("div.datetimepicker").find("input, .input-group-addon").off("mousedown");
    }
};

Set Lookup ReadOnly

SetLookupFieldReadOnly = function (fieldName, readOnly) {
    if (readOnly) {
        $('#' + fieldName).siblings("div.input-group-btn").find("button").prop("disabled", true);
        $('#' + fieldName).siblings("div.input-group-btn").hide();
    } else {
        $('#' + fieldName).siblings("div.input-group-btn").find("button").prop("disabled", false);
        $('#' + fieldName).siblings("div.input-group-btn").show();
    }
};

Set Checkbox ReadOnly

SetCheckboxFieldReadOnly = function (fieldName, readOnly) {
    if (readOnly) {
        $('#' + fieldName).prop("disabled", true);
    } else {
        $('#' + fieldName).prop("disabled", false);
    }
};

Set Radio Button ReadOnly

SetRadioFieldReadOnly = function (fieldName, readOnly) {
    if (readOnly) {
        $('#' + fieldName).find("input[type='radio']").prop("disabled", true);
    } else {
        $('#' + fieldName).find("input[type='radio']").prop("disabled", false);
    }
};

Set Dropdown ReadOnly

SetDropdownFieldReadOnly = function (fieldName, readOnly) {
    if (readOnly) {
        $('#' + fieldName).attr("readonly", true);
        $('#' + fieldName).css("pointer-events", "none");
    } else {
        $('#' + fieldName).attr("readonly", false);
        $('#' + fieldName).css("pointer-events", "auto");
    }
};

We still need one more function to cater for any other datatype. This will be the main function that we will make the call and we need one additional parameter, representing the field datatype.

This function will validate the field type and call the appropriate function we created above (via switch case), finally having the generic read-only function in the default instruction:

Set Field ReadOnly Function

SetFieldReadOnly = function (fieldName, readOnly, type) {
    try {
        type = type.toLowerCase();

        switch (type) {
            case "date":
            case "time":
            case "datetime":
                SetDateTimeFieldReadOnly(fieldName, readOnly);
                break;
            case "lookup":
                SetLookupFieldReadOnly(fieldName, readOnly);
                break;
            case "checkbox":
                SetCheckboxFieldReadOnly(fieldName, readOnly);
                break;
            case "radio":
                SetRadioFieldReadOnly(fieldName, readOnly);
                break;
            case "dropdown":
                SetDropdownFieldReadOnly(fieldName, readOnly);
                break;
            default:
                if (!!readOnly) {
                    $("#" + fieldName).attr("readonly", true);
                    $("#" + fieldName).css("cursor", "not-allowed");
                    $("#" + fieldName).on("mousedown", function (e) { e.preventDefault(); return false; });
                } else {
                    $("#" + fieldName).attr("readonly", false);
                    $("#" + fieldName).css("cursor", "default");
                    $("#" + fieldName).off("mousedown");
                }
                break;
        }
    }
    catch (err) {
        console.error("Error SetFieldReadOnly: " + err.message);
        return;
    }
};

Now you can combine all JavaScript code above, saving as a .js file (and upload as a Web File), or a Web Template record, or a Content Snippet record, etc. Then refer to it in any Portal page, or even the Tracking Code Content Snippet (so it can be used in every page) – and that’s it, the functions are ready to be used in your Portals.

Posted in Dynamics 365

Dynamics 365: Web Service plug-in failed – Invalid name character in ‘List`1’ when using Shared Variables

Hi

Recently I had an issue with D365 plug-ins and it took me a lot of investigation and trial & error to figure out the problem.

I will start by explaining my scenario:

  • Dynamics 365 v8.2 on-premise
  • Plug-in registered on the Update message and Pre-Validation execution pipeline
  • Plug-in registered on the Update message and Pre-Operation execution pipeline
  • Also my Pre-Validation plug-in passes two Shared Variables to my Pre-Operation plug-in

If you are not familiar with Shared Variables in plug-ins, they are a great way to pass values through custom code without having to read D365 data again. It is also a great way to avoid infinite loops. Take a look at the documentation for more details: https://docs.microsoft.com/en-us/dynamics365/sales-enterprise/developer/custom-plugin-handling-shared-variable.

When triggering the plug-in via my application, it threw a very weird exception and for some reason I couldn’t debug my plug-in (a serialization error would come up and not log into the profiler). After a while troubleshooting, I found the below error in the Event Viewer in D365 Server:

The Web Service plug-in failed in OrganizationId: 466f6c23-6896-e811-a824-005056a99671; SdkMessageProcessingStepId: 4d51b0c7-d9c4-ea11-b81d-005056a75ee5; EntityName: contact; Stage: 30; MessageName: Update; AssemblyName: Microsoft.Crm.Extensibility.InternalOperationPlugin, Microsoft.Crm.ObjectModel, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35; ClassName: Microsoft.Crm.Extensibility.InternalOperationPlugin; Exception: Unhandled Exception: System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Object[] arguments, Signature sig, Boolean constructor)
   at System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Object obj, Object[] parameters, Object[] arguments)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Web.Services.Protocols.LogicalMethodInfo.Invoke(Object target, Object[] values)
   at Microsoft.Crm.Extensibility.InternalOperationPlugin.Execute(IServiceProvider serviceProvider)
   at Microsoft.Crm.Extensibility.V5PluginProxyStep.ExecuteInternal(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.VersionedPluginProxyStepBase.Execute(PipelineExecutionContext context)
Inner Exception: System.ArgumentException: Invalid name character in 'List`1'. The '`' character, hexadecimal value 0x60, cannot be included in a name.
   at System.Xml.XmlWellFormedWriter.CheckNCName(String ncname)
   at System.Xml.XmlWellFormedWriter.WriteQualifiedName(String localName, String ns)
   at System.Runtime.Serialization.XmlWriterDelegator.WriteAttributeQualifiedName(String attrPrefix, XmlDictionaryString attrName, XmlDictionaryString attrNs, XmlDictionaryString name, XmlDictionaryString ns)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteTypeInfo(XmlWriterDelegator writer, XmlDictionaryString dataContractName, XmlDictionaryString dataContractNamespace)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteTypeInfo(XmlWriterDelegator writer, DataContract contract, DataContract declaredContract)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithXsiType(XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle objectTypeHandle, Type objectType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle, Type declaredType)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteKeyValuePairOfstringanyTypeToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract )
   at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteParameterCollectionToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , CollectionDataContract )
   at System.Runtime.Serialization.CollectionDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteAsyncOperationDataToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract )
   at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteAsyncOperationDataToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract )
   at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteAsyncOperationDataToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract )
   at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerialize(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.InternalSerializeReference(XmlWriterDelegator xmlWriter, Object obj, Boolean isDeclaredType, Boolean writeXsiType, Int32 declaredTypeID, RuntimeTypeHandle declaredTypeHandle)
   at WriteAsyncOperationDataToXml(XmlWriterDelegator , Object , XmlObjectSerializerWriteContext , ClassDataContract )
   at System.Runtime.Serialization.ClassDataContract.WriteXmlValue(XmlWriterDelegator xmlWriter, Object obj, XmlObjectSerializerWriteContext context)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.WriteDataContractValue(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.XmlObjectSerializerWriteContext.SerializeWithoutXsiType(DataContract dataContract, XmlWriterDelegator xmlWriter, Object obj, RuntimeTypeHandle declaredTypeHandle)
   at System.Runtime.Serialization.DataContractSerializer.InternalWriteObjectContent(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at Microsoft.Crm.Extensibility.AsynchronousStep.SerializeAsyncData(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.AsynchronousStep.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.PipelineInstrumentationHelper.Execute(Boolean instrumentationEnabled, String stopwatchName, ExecuteWithInstrumentation action, PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.Pipeline.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.PipelineInstrumentationHelper.Execute(Boolean instrumentationEnabled, String stopwatchName, ExecuteWithInstrumentation action, PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.MessageProcessor.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.InternalMessageDispatcher.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.ExtensiblePlatformMessageDispatcher.Execute(PipelineExecutionContext pluginContext)
   at Microsoft.Crm.Extensibility.ExtensiblePlatformMessageDispatcher.UpdateWithInvocationSource(BusinessEntity entity, FilterExpression filter, Int32 invocationSource, ExecutionContext context)
   at Microsoft.Crm.Extensibility.ExtensiblePlatformMessageDispatcher.Update(BusinessEntity entity, FilterExpression filter, ExecutionContext context)
   at Microsoft.Crm.BusinessEntities.BusinessProcessObject.UpdateWithPipelineAndExtensions(IBusinessEntity entity, ExecutionContext context)
   at Microsoft.Crm.BusinessEntities.BusinessProcessObject.Update(IBusinessEntity entity, ExecutionContext context)
.

The above error message doesn’t really help much as it happens in an internal plug-in.

Also this sentence from the error message didn’t make a lot of sense (at first): Invalid name character in ‘List`1’. The ‘`’ character, hexadecimal value 0x60, cannot be included in a name.

In my code I wasn’t referencing anything that could have a “`” or any other special character that could be messing things up. My code in the pre-validation plug-in was running correctly and I was able to debug it, I just couldn’t debug the pre-operation one.

After a while troubleshooting and nearly giving up, a colleague suggested that I play around with the Shared Variables I was passing between the plugins and bang!!! That was the issue…

I had two values passing via Shared Variables:

  • GUID – created in the Pre-Validation step
  • List<Entity> – records retrieved in the Pre-Validation step

When removing the List object from my Shared Variables, my plug-in started working with no issues and I could even debug my Pre-Operation plug-in.

Why does this happen?

The issue happens due to how the C# language names a generic list object. Running the simple code below, the exception starts making sense:

            List<string> myList = new List<string>();

            myList.Add("value 01");
            myList.Add("value 02");
            myList.Add("value 03");

            foreach (var item in myList)
            {
                Console.WriteLine(item);
            }

            Console.WriteLine(myList.ToString());

Internally, D365 is probably trying to convert the list object into a string (myList.ToString()) to be used in the next plug-in, and due to the special character created by C#, it throws an exception that Dynamics 365 can’t handle.

Conclusion

The conclusion for this is very simple, don’t pass a list of objects via Shared Variables in plug-ins. In my case, I decided to change the retrieve to the Pre-Operation plug-in. Another thing you can try is an array of GUIDs or other array types, just not a generic C# list object.

Posted in JavaScript/jQuery Plugins, Power Apps Portals

Power Apps Portals – Field Label Position

Hi,

In this post I will show you how to set the field label position within an Entity Form/Web Form in Power Apps Portals.

If you are familiar with Dynamics 365, you probably are aware that we can define which position to place a field label within a form. The positions available are:

  • Side: label and input control are positioned side-by-side
  • Top: label and input control are positioned as top-bottom

Unfortunately, Power Apps Portals ignores this definition and always renders your form as top-bottom positioning:

I will show you how to change this behavior to render your field label as side-by-side position. We can achieve this via JavaScript or CSS:

Via JavaScript/jQuery

If you need to set only for a specific form or perhaps specific section in your form, you can use the following jQuery function (just relace the sectionGeneral below for your section name):

$("table[data-name|='sectionGeneral']").find(".control").css("clear", "none");
$("table[data-name|='sectionGeneral']").find(".control").css("float", "right");

Via CSS

If you want to setup this behavior for every Entity Form/Web Form within your Portals, or perhaps for an entire Web Page, you can set it via CSS (you can place this in your .css file or the Web Page custom css field):

.section tbody tr td div.control{
    clear: none !important;
    float: right;
}

Now refresh your Portals Web Page and you will have the below rendered form (don’t forget to clear the cache):

Posted in Portal Management App, Power Apps Portals

Power Apps Portals – Script Error: One of the scripts for this record has caused an error…

Hi

This is a quick post that might help you in case you come across the below error message when using the Portal Management App within Power Apps Portals:

The first time I had this issue it took me a while to figure out what was happening. When viewing the error details, all I could see is that the Portals couldn’t run its OOB JavaScript (even though I hadn’t made any changes to it):

ReferenceError: Web resource method does not exist: adx.adx_webpage.loadResources
at Kl.execute (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:1262:2548)
at https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:118:20532
at o (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:289:133)
at $._executeIndividualEvent (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:118:20508)
at $._executeEventHandler (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:118:18113)
at Object.execute (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:118:15469)
at O._executeSyncAction (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:855:692)
at O._executeSync (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:855:419)
at O.executeAction (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:855:201)
at t.dispatch (https://orgf67a233b.crm4.dynamics.com/uclient/scripts/app.js?v=1.4.1024-2006.4:855:5545)

Fixing this problem is actually a lot easier than it looks. The root cause of this is actually an Ad Block extension that you might have installed in your browser. This issue happened to me with a few different extensions in multiple browsers.

The JavaScript used by the Portal Management App probably uses similar functions as common ads all over the internet, so it ends up blocked, causing several issues when trying to configure Portal data.

To fix this all you need is to either remove the extension, or add an exception based on the website URL. This may vary depending on the extension you have installed, in my case I have the AdBlock for Edge, so I can setup this exception using the option “Don’t run on pages on this site.”

Conclusion

If you have this problem, don’t stress, you didn’t do anything wrong – just follow the steps above and the error message will be gone!

Posted in Caching, Power Apps Portals

Power Apps Portals – Caching Tips

My record data is not up to date

My portal configuration is not showing the latest definition

I can’t see the Clear Cache button

The above are very common questions as regards Power Apps Portals Caching. In this post I will give you guys a few tips on how to identify/solve some of these issues.

A few things are very important to know about how Power Apps Portals uses CDS Dataflex Pro data:

  • Power Apps Portals shares the same database as your Model-Driven App, it doesn’t have a database copy and there is no data sync involved
  • A Server-Side Caching is part of the Portals capabilities to improve performance, a message is transferred asynchronously between your Dataflex Pro and the Portals to notify (invalidate) the data once it’s changed
  • The SLA for message transfer is 15 minutes
  • Cache is automatically invalidated when sync transactions are triggered via Portals
  • When a D365 org is reset or restored, cache invalidation can stop working. To enable it, go to Portal Admin Center > Portal details tab > Press Update
  • Clear Cache function is available on <Portal URL>/_services/about (must be logged in as Administrator)
  • No web notification invalidation plugin is required to invalidate cache (this was legacy on ADX Portals)

The above should give you a better understanding of how it works. Let me share a few more tips with you:

Clear Cache function in Bookmark

Add the below JavaScript code into a bookmark in your browser, this will open a separate tab with the Clear Cache page, regardless of which Portals/Page you have opened.

javascript:((function(){window.open(window.location.origin+"/_services/about", "_blank");})())

Clear Cache page

The Clear Cache page shows information about your current Power Apps Portals, like version, Organization ID, etc. Three buttons are available for caching actions:

  • Clear cache: clear cache for both transaction and configuration entities
  • Rebuild search index: rebuild full search index, to be used when Global Search configurations are changed
  • Clear config: clear cache for adx_* entities

Administrator Web Role

If you see a blank page when opening the clear cache page, it means you are not logged in as an Administrator user.

Go to https://make.powerapps.com > Apps > Portal Management App > Contact > find your Contact record and under related entities go to Web Roles. Make sure you have the Administrators Web Roles associated to your Contact.

Clear Cache using Portal Studio – Sync Configuration

Another way to clear the cache is using Portal Studio capabilities, to use this option, go to https://make.powerapps.com > Apps > Select your Portal app and click Edit:

Click on the Sync Configuration button to clear the cache:

Clear Cache using Portal Admin Center

Also, another way to clear the cache is by using Portal Admin Center actions. To use this option, go to https://make.powerapps.com > Apps > Select your Portal app click Settings and click Administration:

On the left menu click on Portal Actions and click the Restart option:

This will restart the website app service and clear the server-side cache.

This option might take your Portal offline for a few minutes, so be aware of that when performing this action.

Can I Clear Cache Programmatically?

No. Currently there is no supported way to clear cache programmatically, so it is not possible to automate or trigger it programmatically.

Retrieving data via FetchXML/Liquid or oData

If you are retrieving data programmatically in the Portals, it’s possible to bypass the cache by making sure your query is always unique. A simple way of achieving this is by adding a filter with a timestamp, for example:

  • oData – JavaScript
var oDataURL = "/_odata/myoDataSet";
var filter = "?$filter=new_name ne '" + new Date().getMilliseconds() + "'";
  • FetchXML – Liquid

In my example, I first create a variable and then add it as a condition in my FetchXML. The only reason I do this is to make it easy to reuse the code in different places, you can even create a Web Template just returning that filter condition and reuse it by using the include tag (maybe this is a topic for another post):

{% assign currentDate = now | date: "yyyyMMddHHmmss" %}
{% assign conditionCache = ' <condition attribute="new_name" operator="ne" value="' | append: currentDate | append: '" />' %}

Then you just add the condition in your FetchXML:

{% fetchxml myFetch %}
      <fetch>
        <entity name="new_entityname">
          <filter type="and">
            {{ conditionCache }}
          </filter>
        </entity>
      </fetch>
{% endfetchxml %}

CSS changes

When making changes related to styling/CSS, your local browser cache might be influencing here as well. You might consider trying an in-private/incognito session or clearing the local browser’s cache.

Thanks Nikita Polyakov for this tip. 

Conclusion

Microsoft is constantly making improvements related to Portal caching. But for now, we still have to live with some of the above issues.

I hope this post has been useful for you to get a better understanding of how Caching works, and how to overcome potential obstacles you might run into along the way.

Posted in JavaScript/jQuery Plugins, Power Apps Portals

Power Apps Portals – JavaScript Tip #01 – Hide & Show Elements

Power Apps Portals allows us to add custom JavaScript/jQuery code to manipulate behaviour within the Portals website.

I am starting a series of quick posts with snippets of JavaScript/jQuery code to help you with your Power Apps Portals implementation.

When talking about client-side customization, there is no single way of interacting with the page elements, there are several ways to achieve same results, in this post I will give an example on how to hide & show elements in your page, so here we go:

Hide & Show Fields

$("#<field name>").closest("td").find("div.control, div.info").hide(); // show();

Hide & Show Section

$("table[data-name='<section name>']").parent().hide(); // show();

Hide & Show Tab

$("div[data-name='<tab name>']").prev("h2.tab-title").hide();  // show();
$("div[data-name='<tab name>']").hide();  // show();

Hide & Show Entity List/Sub-grid column

var list = $(".entity-grid")
list.on("loaded", function () {
	list.find("th:contains('<column display name>')").hide(); // show();
	list.find("td[data-th='<column display name>']").hide(); // show();
}); 

Hide & Show Option Set value

$("#<field name> option[value='< option set value>']").hide(); // show();

Do you have any suggestions or requests for another JavaScript tip? Let me know in the comments below.

Posted in Menu / Navigation, Power Apps Portals

Power Apps Portals – Navigation Menu showing every Web Page

Have you recently come across a situation where after defining the links for your main navigation menu, it suddenly resets on its own showing every custom Web Page you’ve created? Well, in this post I am going to show you how to stop that from happening.

Behind the scenes

First, we need to understand how the menu is structured. A menu is represented by two entities in your CDS environment:

  • Web Link Set: represents a group of links, the main menu for example is represented by the Default Web Link Set
  • Web Link: represents the link to a certain page in your Portal, or an external URL, you can also define an image to be displayed and the order to be shown in your Web Link Set

For more information on managing Web Link Set/Web Link, please refer to the official documentation: https://docs.microsoft.com/en-us/powerapps/maker/portals/configure/manage-web-links.

Finally these entities are represented as custom Liquid objects, being rendered in the Header or Footer Web Template.

What is causing this menu reset?

Now let’s understand exactly what is happening with the Portals.

Microsoft is making a lot of investment in Portals Studio app (https://docs.microsoft.com/en-us/powerapps/maker/portals/portal-designer-anatomy) adding lots of new features on a regular basis. Personally, I am not a big fan of the Portals Studio and I still go to the classic Portal Management app. If you are new to the Portals or just want to quickly update certain content, I completely agree that the Portals Studio interface is much easier and intuitive to make simple changes.

One important (and hidden) feature of the Portals Studio app, is that it automatically refreshes the Default Web Link Set with any custom Web Page you may have created. It doesn’t matter if you’ve created the Web Page via Portal Studio or Portal Management, this seems to happen on the onload event of the Portals Studio, but at the same time it doesn’t show you straight away, only after refreshing the Portals Studio page.

A bit confusing? Let’s try to replicate the issue step-by-step:

  • Open two tabs in your browser, one with your Portals website and the other the Portal Studio App, at this moment the navigation menu should be identical in both tabs
  • Create a new Web Page (can be a blank page), sync and refresh first just the Portals website
  • Now your new Web Page is present in the menu

Now let’s go to the Portal Management App, navigate to the Web Link Set and open the Default Web Link Set. In the Web Links tab, you will see the recently created Web Page there as a new Web Link, even though you didn’t want it to be shown there.

Got it, now what’s the solution?

I know the above text was a bit long, but I think it is important to understand what’s happening in the background. There are a few ways to fix this.

Within the Portals Studio, you can hide the page from showing in the menu, what this will do is deactivate the Web Link record in the CDS, so it won’t show in the menu anymore. Just go to Pages, select the ellipses (…) on your page and click Hide in default menu:

But the problem is still there, if you create new pages it will keep adding to the menu. To get rid of this behavior, we need to change the reference to the Web Link Set. Let’s just forget about the Default Web Link Set and leave the Portals manage it the way it wants, we will be using the Primary Navigation Web Link Set for our menu. In the Portal Management App add/remove any links you want in your menu.

Once the menu has been defined in the Portal Management App, we need to set the Portals to use that Web Link Set, there are two ways of doing this. Please read the entire post before actually making any change in your environment:

Set Navigation Menu via Portals Studio

Open the Portals Studio and click on the main menu, and on the right-hand pane, change the navigation menu dropdown:

Click sync configuration and you’re done!

Set Navigation Menu via Portal Management App / Header Web Template

Go to the Web Templates, open the Header record. Simply change the reference to the Web Link Set, locate the line below, and replace Default for Primary Navigation:

Now clear the cache and don’t worry about this issue again.

Which method should I use?

One thing I noticed is that when changing the navigation menu via Portals Studio, the Portals automatically resets the Header Web Template with the OOB code, changing only the line mentioned above. If you haven’t made any customization in your Web Template, this won’t be an issue, and it is probably easier to change, but if you have made any changes in the Liquid code, you definitely don’t want to lose them, so you’re better off making the change manually in the Web Template.

Apparently this was a bug and it is now fixed in the version 9.2.5.x, although I currently find anywhere in the release notes, when testing this was now updating only the weblinkset assignment, instead of the entire Web Template

Update on 20th July 2020

I see a lot of people having issues with the menu navigation where the pages keep being added, I hope this post clarifies why this is happening and how to solve it.