Have you had to come up with a solution to display a single PowerApps-based app in multiple Dynamics 365 forms? Currently, you cannot use the same app you embed using the Dynamics 365 Form control (new App ID is created during this process) in another Dynamics 365 entity. So, if you need to use the same app and make it editable in other entities, there’s no easy solution. In today’s blog, we will cover how you can use Dynamics 365’s embedding control along with iFrame and Java Script to accomplish this. At the end, we will cover why you should use Common Data Service (CDS) Connector over Dynamics 365 Connector for the PowerApps.

Embedding PowerApps using Dynamics 365 Control is simple – just follow these steps:

1. Click Set Properties for the Field to which you want to add the PowerApps control.

2. On the Controls tab, click Add Control and select Canvas app (preview).

3. Check the Web box.

4. Select Customize.

NOTE: PowerApps control currently works only for a Single Line of Text field.

For more information on Embedded PowerApps, visit: https://www.powerobjects.com/how-to-embed-powerapps-in-crm-without-writing-code/.

To embed PowerApps as an iFrame on an entity form, follow these steps:

1. Select Insert from the main Tab.

2. Select IFRAME.

3. Provide the Name and URL.

For more information of iFrame, visit: https://docs.microsoft.com/en-us/powerapps/maker/model-driven-apps/iframe-properties-legacy

See below to copy JavaScript. In this JavaScript, you’ll notice the code is registered on Tab state change on the below entities:

1. Accounts (Primary Entity)

2. Opportunities

3. Leads

4. Phone Calls

5. Appointment

/*This code is registered on Tab state change on these entities:

Accounts
Opportunities
Leads
Phone Calls
Appointment

*/
function nameofthefunction(executionContext) {
    var formContext = executionContext.getFormContext();
    var appUrl = getPowerAppUrl(formContext);
    var entityName = formContext.data.entity.getEntityName();
    
    //if statement used to return accound id for records directly associated to an account, OR the else statement uses a .then() promise to make a retrieverecord call first
    if(entityName === "account" || entityName === "lead" || entityName === "opportunity"){
        var recordID;
        recordID = getAccountID(entityName, formContext);
         //console.log('in nonPROMISE');
            
        //if account is not null do a thing
        
        //console.log('recordID', recordID);
        
        if (recordID != null) {
            //console.log('in success');
            /* do something with the result */
           
            //Get the default URL for the IFRAME, which includes the   
                    // query string parameters  
                    var IFrame = formContext.ui.controls.get("IFRAME_nameoftheIFrame");
                    var newTarget = appUrl + "&AccountID=" + recordID;
                    // Use the setSrc method so that the IFRAME uses the  
                    // new page with the existing parameters
                    //console.log('newTarget:', newTarget);  
                    IFrame.setSrc(newTarget);
        } else {
            //if account is null, throw an error
            //console.log('in reject');
         
        };

    } else{
        //else statement for activities (phone call and appointment)
        //check if regardingobjectid contains a value so it doesn't throw a JS error in the browser if no parent exists
        //console.log('test', formContext.getAttribute("regardingobjectid").getValue());
        
        if(formContext.getAttribute("regardingobjectid").getValue() != null){
            //else statement uses .then() promise for processes that require a retrieverecord call
            getAccountID(entityName, formContext).then(function(recordID){
            
            //console.log('in mypromise');
                
            //if account is not null do a thing
            
            //console.log('recordID', recordID);
            
            if (recordID != null) {
                //console.log('in mypromise success');
                /* do something with the result */
            
                //Get the default URL for the IFRAME, which includes the   
                        // query string parameters  
                        var IFrame = formContext.ui.controls.get("IFRAME_nameoftheIFrame");
                        var newTarget = appUrl + "&AccountID=" + recordID;
                        // Use the setSrc method so that the IFRAME uses the  
                        // new page with the existing parameters
                        //console.log('newTarget:', newTarget);  
                        IFrame.setSrc(newTarget);
            } else {
                //if account is null, throw an error
                //console.log('in mypromise reject');
            };
            });
        };
    };
}

///////////////////////// getAccountID /////////////////////////////
//directly returns the account id for account records or any record directly associated to the account
//will also return the account id for any activity records that are another step away from the account through getAccountFromActivity() (such as a phone call or appointment on a lead that lookups up to an account)
function getAccountID(entityName, formContext) {
    var value;
    
    switch (entityName) {
        //Non-Activity Entities
        case "account":
            //does NOT require promise via getAccountFromActivity()
            value = formContext.data.entity.getId();

            if (value != null) {
            value = cleanGUID(value);
            return value;
            }
            break;
            
        case "opportunity":
            //does NOT require promise via getAccountFromActivity()
            getAccountFromActivity()
            //if account is not selected, avoid JS error 
            if(formContext.getAttribute("parentaccountid").getValue() != null){
                value = formContext.getAttribute("parentaccountid").getValue()[0].id;
                value = cleanGUID(value);
                return value;
                }
            break;
            
        case "lead":
            //does NOT require promise via getAccountFromActivity()
            getAccountFromActivity()
            //if account is not selected, avoid JS error 
            if(formContext.getAttribute("accountid").getValue() != null){
                value = formContext.getAttribute("accountid").getValue()[0].id;
                value = cleanGUID(value);
                return value;
            }
            break;
            
        case "phonecall": 
        case "appointment": 
        //Activity Entities
        //REQUIRES promise via getAccountFromActivity()       
            regardingId = cleanGUID(formContext.getAttribute("regardingobjectid").getValue()[0].id);
            entityName = formContext.getAttribute("regardingobjectid").getValue()[0].entityType;
            value = getAccountFromActivity(regardingId, entityName, formContext);
            return value;
        }
    //return the account ID
    return value;
   
}

///////////////////////// cleanGUID /////////////////////////////
//Pass an object to this function. 
//It will get the formatted GUID,strip the braces and lowercase all apha chars 
function cleanGUID(dirtyGUID) {

    if (dirtyGUID != null) {
        cleanValue = dirtyGUID.replace("{", "").replace("}", "").toLowerCase();
        //console.log('cleanValue', cleanValue);
        
    }
    return cleanValue;
}

///////////////////////// getPowerAppUrl /////////////////////////////
//Based on the org, direct the user to the right PowerApp
function getPowerAppUrl(formContext) {

    powerAppsBase = "https://web.powerapps.com/webplayer/iframeapp?source=iframe&appId=/providers/Microsoft.PowerApps/apps/";
    url = formContext.getUrl();

    if (url && url != "") {

        if (url.search("orgname1") > 0) return powerAppsBase + "appId1";
        else if (url.search("orgname2") > 0) return powerAppsBase + "appID2";
        else if (url.search("orgname3") > 0) return powerAppsBase + "appID3";
        else if (url.search("orgname4") > 0) return powerAppsBase + "appID4";
        else if (url.search("orgname5") > 0) return powerAppsBase + "appID5";
    }
}

///////////////////////// getAccountID /////////////////////////////
//returns account id for activity records
function getAccountFromActivity(regardingId, regardingEntityType, formContext){
    var accountId;
    return new Promise(function (resolve, reject) {
        //promise allows retrieverecord web call to call back before the rest of the code runs
    if (regardingEntityType != null) {
        //switch implemented here because there are different fields 
        //that we need to look at to get an Account ID depending 
        //on the entity that is in an Activity's 'regarding' field.
        switch (regardingEntityType) {
            case "account":
                resolve(regardingId);
                break;
            
            case "lead":
                //console.log('regardingId', regardingId);
                Xrm.WebApi.retrieveRecord("lead", regardingId, "?$select=leadid,_accountid_value").then(
                    function success(result) {
                        //console.log('expanded query result', result);
                        accountId = result["_accountid_value"];
                        //resolves the .then() promise
                        resolve(accountId);
                    },
                    function(error) {
                        Xrm.Utility.alertDialog(error.message);
                    }
                );
                break;
               
            case "opportunity":
                Xrm.WebApi.retrieveRecord("opportunity", regardingId, "?$select=_parentaccountid_value").then(
                    function success(result) {
                        accountId = result["_parentaccountid_value"];
                        resolve(accountId);
                    },
                    function(error) {
                        Xrm.Utility.alertDialog(error.message);
                    }
                );
                break;
               
            case "contact":
                Xrm.WebApi.retrieveRecord("contact", regardingId, "?$select=_parentcustomerid_value").then(
                    function success(result) {
                        accountId = result["_parentcustomerid_value"];
                        resolve(accountId);
                    },
                    function(error) {
                        Xrm.Utility.alertDialog(error.message);
                    }
                );
                break;
    }
    }
})
}

// Displays Tab only if Type = Blog Activity
function showHideActivityTab(executionContext)
{
    var formContext = executionContext.getFormContext();
     activityType = formContext.getAttribute("typecode").getText();
     if (activityType != undefined 
         && activityType != null){
            if(activityType == "Blog Activity"){
                formContext.ui.tabs.get("nameyouset").setVisible(true);
            }
            else if(activityType !== "Blog Activity"){     
                formContext.ui.tabs.get("nameyouset").setVisible(false);
            }
    }
}

Once the JavaScript is ready, add iFrames in the tabs on which you want to display the PowerApps. To call a function from your JavaScript on Tab change, follow the steps below:

1. In Tab Properties, select Events.

2. Add your JavaScript from the Library by clicking +.

3. Double-click the JavaScript to open Handler Properties.

4. Call the Function from your JavaScript.

5. Check the Enabled and Pass execution context as first parameters boxes.

6. Save and Publish.

Now that everything is ready to go, let’s discuss why you would want to use Common Data Service Connector over Dynamics 365 Connector.

If you use Dynamics 365 Connector within the same tenant and different orgs, every time you export/import the app in another environment, you will need to delete the existing data sources and re-add the data sources so that the app can connect to the correct environment’s data source. If you are importing the PowerApps from one tenant to another tenant, it automatically connects to that environment’s data sources.

On the other hand, if you are using Common Data Service (CDS) Connector within the same tenant and different orgs, you do not need to delete the existing data sources after you import the app. CDS has a feature called Current environment that, upon import of the app in a different environment, uses the data source from the current environment.

Be sure to subscribe to our blog for more PowerApps tips and tricks.

Happy Dynamics 365’ing!

Avatar for Joe D365

Joe D365

Joe D365 is a Microsoft Dynamics 365 superhero who runs on pure Dynamics adrenaline. As the face of PowerObjects, Joe D365’s mission is to reveal innovative ways to use Dynamics 365 and bring the application to more businesses and organizations around the world.