Troubleshooting Business Rules

Business Rules is composed of two components: a client side JavaScript and a server side workflow. You can use these queries to find out the details about this:

LinqPad – Query and result

business-rule-linqpad

FetchXml Builder – Query and result

fetchxml-query

fetchxml-result

One gotcha with the business rule is that its behavior will deviate, if a field that is required by the business rule is removed on the form. I will demonstrate this, with the business rule below.

contracting-unit-is-required

This business rule sets the “Contracting Unit” to required, if Order Type is “Work Based”. Below are the screenshots of the form in two different scenarios:

With “Order Type” present on the form

with-order-type

Without “Order Type” present on the form

Without Order Type.png

As you can see, “Contracting Unit” is set to required, only if the “Order Type” field is present on the form, even if the value of “Order Type” is “Work based”. At present, there seems to no check in the form customisation area to prevent a field from being removed, if is required by a business rule. This is how the “Contracting Unit is required” business rule, gets translated into JavaScript.

business-rule-chrome-devtools

When you debug this using Chrome Dev Tools, you can easily see why the “Contracting Unit” field is not being set to required.

business-rule-chrome-devtools-debug

To assist developers who are troubleshooting why a business rule is not working, I have developed this simple script to run in the DevTools console, that lists the fields that are required by the Business Rules, but are not present in the form. This has to be run in the context of ClientApiWrapper IFrame.

let formAttribs = Xrm.Page.getAttribute().map(a=>a.getName()); Object.keys(Mscrm.BusinessRulesScript.AttributesOnChangeHandlers).filter(x=>!formAttribs.includes(x))

Here is a sample output of this script.

business-rule-field-dependencies

It is saying that “Order Type” should be present in the form, as it is required by a Business Rule that is running on the form. It uses an unsupported internal method to identify this information, and so I recommend that it be used in devtools console only.

Further Reading:

Understanding Process Triggers and Business Rule internals

Advertisements

Cherry picking Xrm.Internal

EDIT (09/02/17): This post is a result of my exploration into the Xrm.Internal namespace as other internal CRM code in general. It is not a recommendation to use this in a Production environment, as it is UNSUPPORTED. Since there are already ways to run SDK messages from JavaScript using many open source frameworks, I have decided not to post part 2 (how to do this using Xrm.Internal).

Word of warning: Code heavy post and nothing shown here is supported, as it uses internal methods. All these methods have been tested only in Dynamics 365 Online.

Xrm.Internal seems to be a goldmine for cool new functionalities, that one day will eventually make into the Client API. I will document some of the some methods that are useful.

Filter Partylist entity types – Hide unwanted

Scenario: You have opened a new email form. But, you don’t want users to choose contact or lead.

Method Definition: Xrm.Internal.filterLookupTypes([ATTRIBUTE],[ENTITY ARRAY], [FILTER TYPES])

Example

Xrm.Internal.filterLookupTypes(Xrm.Page.getAttribute(“to”), [‘contact’,’lead’], true)

Before

partylist-filter-before

After

partylist-filter-after

Filter Partylist entity types – Show wanted

This is the reverse of the previous scenario. You now want to show only contact and lead. In this case you can use this code.

Xrm.Internal.filterLookupTypes(Xrm.Page.getAttribute(“to”), [‘contact’,’lead’], false)

The order in which the entities appear matter. You can control the resolution order of the entities with this. The first entity on the array is the default selected entity type.

With parameter [‘contact’,’lead’]contact-first

With parameter [‘lead’,’contact’]

lead-first

In both these cases, only the lead and contact entities are shown.

lead-and-contact

Get entity code from schema name

Method Definition: Xrm.Internal.getEntityCode([ENTITY SCHEMA NAME])

Example

Xrm.Internal.getEntityCode(‘contact’)

entity-code-from-name

Get entity display name from schema name

Method Definition: Xrm.Internal.getEntityDisplayName([ENTITY SCHEMA NAME])

Example

Xrm.Internal.getEntityDisplayName(‘contact’)

entity-display-name-from-name

Get entity schema name from objecttypecode

Method Definition: Xrm.Internal.getEntityName([ENTITY OBJECT TYPE CODE])

Example

Xrm.Internal.getEntityName(112)

entity-name-from-otc

Get state code name from integer value

Method Definition: Xrm.Internal.getStateFromNumber([ENTITY SCHEMA NAME],[STATECODES ARRAY])

getStateFromNumber returns a promise, not a simple value.

Example

Xrm.Internal.getStateFromNumber(‘contact’,[0,1]).done(x=>console.log(x))

state-code-name-from-int

Get all features enabled for the current instance

One thing that caught my eye in the feature named “ServeStaticResourcesFromAzureCDN”. I know that learning path assets can be served from Azure CDN. Could this be opened up for other webresources in the future or is this strictly learning path only? I am not sure. Could “ODataV4UI” be Swagger UI or some sort of API Playground? Intriguing.

console.table(Object.keys(Mscrm.FeatureNames).filter(x=>!x.startsWith(‘_’)).map(x=> ({Feature: x, Enabled: Xrm.Internal.isFeatureEnabled(Mscrm.FeatureNames[x])})))

feature-list

 

 

 

Export to Excel using Dynamics 365 SDK

With the new Dynamics 365 release, a new message has been added that making exporting FetchXML results really simple. This message is not documented, hence is technically unsupported. With that word of warning, I will show you how to utilise this new message to export data from Dynamics 365 to an Excel file.

ExportToExcel message definition

Parameter Type
View EntityReference
FetchXml string
LayoutXml string
QueryApi string
QueryParameters InputArgumentCollection

Code

using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.ServiceModel;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Client;
using Microsoft.Xrm.Client.Services;
using Microsoft.Xrm.Sdk;

namespace Experiments
{
    class Program
    {
        private static OrganizationService _orgService;
        static void Main(string[] args)
        {
            try
            {
                CrmConnection connection = CrmConnection.Parse(
                    ConfigurationManager.ConnectionStrings["CRMConnectionString"].ConnectionString);

                using (_orgService = new OrganizationService(connection))
                {
                    var exportToExcelRequest = new OrganizationRequest("ExportToExcel");
                    exportToExcelRequest.Parameters = new ParameterCollection();
                    //Has to be a savedquery aka "System View" or userquery aka "Saved View"
                    //The view has to exist, otherwise will error out
                    //Guid of the view has to be passed
                    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object> ("View",
                        new EntityReference("userquery", new Guid("{0B915102-24A7-E611-8101-1458D05B1178}"))));
                    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("FetchXml", @"
                    <fetch distinct='false' no-lock='false' mapping='logical' returntotalrecordcount='true'>
	                    <entity name='contact'>
		                    <attribute name='fullname' />
	                    </entity>
                    </fetch>"));
                    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("LayoutXml", @"
                    <grid name='resultset' object='2' jump='fullname' select='1' icon='1' preview='1'>
	                    <row name='result' id='contactid'>
		                    <cell name='fullname' width='300' />
	                    </row>
                    </grid>"));
                    //need these params to keep org service happy
                    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("QueryApi", ""));
                    exportToExcelRequest.Parameters.Add(new KeyValuePair<string, object>("QueryParameters",
                        new InputArgumentCollection()));
                    var exportToExcelResponse = _orgService.Execute(exportToExcelRequest);
                    if (exportToExcelResponse.Results.Any())
                    {
                        File.WriteAllBytes("Active Contacts.xlsx", exportToExcelResponse.Results["ExcelFile"] as byte[]);
                    }
                }
            }
            catch (FaultException<OrganizationServiceFault> ex)
            {
                string message = ex.Message;
                throw;
            }
        }
    }
}

Closing Notes:

  1. “View” parameter can accept “userquery” or “savedquery”, but they have to exist i.e. you can’t pass empty Guid.
  2. The fetchxml and layoutxml can be different from what is in the “savedquery” or “userquery”. Hence, you can create a “Personal View” just so that you can use it in this message, but modify the fetchxml and layoutxml to whatever you want.
  3. The name of the tab in the Excel output file will be the name of the view specified in the “View” parameter
  4. This message can be executed from Javascript as well, but you will get a base64 string instead of a byte array in the response.

Please vote up my request on Connect (logged Feb 2015) -> https://connect.microsoft.com/site687/feedback/details/1127874/export-to-excel-sdk-message so that this message can be made available as an unbound WebAPI action.

Understanding Process Triggers and Business Rule internals

One of the less utilised/understood feature of Business Rule is Process Triggers. In this post, I will explain what a process trigger is and how you can use this in the context of business rule.

The Basics

Business Rule is basically a workflow that has a different UI compared to the standard workflow editor. You can quickly find all the business rules in your CRM instance by running this query.

Business Rules

Internals – How does Business Rules work

You can basically skip this part, if you are not interested in understanding the internals on how a business rules work. When you create a business rules you basically have all these components that make the business rules run seamlessly:

  1. The client side code that runs on the form
  2. The server side workflow defined in xaml
  3. Process trigger -> This dictates when the business rule logic should execute

When you design a business rule, it is automatically translated into a workflow xaml that executes on the server side and JavaScript code that executes on the client side.

Now, let us take a simple example of a business rule that sets the “Salutation”, when “Gender” is changed. Here is the business rule

Business Rule Definition

When you save this business rule, this is automatically translated into JavaScript, code that can run on the client side. Below is the JavaScript code that is generated by CRM, for this business rule:

function pbl_109af564df34e51180eac4346bc576e8() {
    try {
        var v0 = Xrm.Page.data.entity.attributes.get('gendercode');
        var v1 = Xrm.Page.data.entity.attributes.get('salutation');
        if (((v0) == undefined || (v0) == null || (v0) === "") || ((v1) == undefined || (v1) == null || (v1) === "")) {
            return;
        }
        var v2 = (v0) ? v0.getValue() : null ;
        if ((v2) === (1)) {
            v1.setValue('Mr');
        } else if ((v2) === (2)) {
            v1.setValue('Ms');
        }
    } catch (e) {
        Mscrm.BusinessRules.ErrorHandlerFactory.getHandler(e, arguments.callee).handleError();
    }
}

Below is the JavaScript code this calls the “pbl_109af564df34e51180eac4346bc576e8” function that contains the logic for the business rule.

Mscrm.BusinessRulesScript.Initialize = function() {
    Mscrm.BusinessRulesScript.AttributesOnChangeHandlers = {};
    Mscrm.BusinessRulesScript.ControlsOnClickHandlers = {};
    (function() {
        var onchangehandler = function() {
            pbl_109af564df34e51180eac4346bc576e8();
        }
        ;
        Mscrm.BusinessRulesScript.AttributesOnChangeHandlers['gendercode'] = onchangehandler;
        var attributeObject = Xrm.Page.data.entity.attributes.get('gendercode');
        if (attributeObject != null && attributeObject != undefined) {
            attributeObject.addOnChange(onchangehandler);
        }
    })();
    pbl_109af564df34e51180eac4346bc576e8();
};

From the above, triggering code we can see that the business rule is going to run when the form is opened, as the function “pbl_109af564df34e51180eac4346bc576e8” is called when the business rule is initiated. The function also executes when “Gender” is changed.

If you want to know what the generated JavaScript code for the business rule is, just get the “ClientData” field in the “Workflow” entity. You cannot get this field from Advanced Find. You can either use FetchXML Builder (a XrmToolBox tool) or LINQPad. Below is the fetchxml query, I used.

FetchXml

If you also get the “xaml” field on the workflow entity, you can see the markup server side workflow logic that will execute.

Process Trigger

Process Trigger dictates the events that will trigger the execution of business rule. There are three events:

  1. Load
  2. Change
  3. Save

“Load” and “Change” are the standard triggers when the business rule is created through the UI. “Save” is a special handler. It can only be set using the SDK and not through the UI. It behaves little differently compared to “Load”. The JavaScript code that is generated for “Save” is little different compared to the code that is generated for “Load”. Here is the LINQ query I used for getting process triggers for this workflow.

LINQPad

Now let us update the “load” trigger to “save”, so that the generated JavaScript for the business rule will run only on “Form Save” event and not on “Form Load”. Here is the simple snippet I ran to do this, after I got the ids of the process trigger in the previous LINQ Query. These process triggers are for the “load” events associated to our business rule. You have to deactivate the business rule, before you update the process trigger for the business rule.

Process Trigger Update

Now comes the important bit: Activate the business rule from the Advanced Find results (first screenshot). Don’t activate the business rule from the standard business rule window. If you activate the business rule from the standard UI, your process trigger will reset back to “load”

ActivateDont Activate

Now that the trigger is set to “Save”, lets compare the generated JavaScript code.

Compare Load and Save

As we can see, in the case of “save” process trigger, the client side business rule code runs only on “Save”. I like this, because I don’t want the business rule to run on “load” and confuse the user with “Unsaved changes” message. I will demonstrate this with a scenario.

Scenario: User opens a contact record, which doesn’t have the “Salutation” field set. Gender contains a valid value. The process trigger is “load”

Form OnLoad

As you can see from the above screenshot, the business rule ran immediately on form load, and set the Salutation to “Mr”. Hence, you have a unsaved changes message, on the bottom right. I am not very happy with this result because, it is not obvious to the user what changed and what caused the change. I want more control, so I want this rule to run only after “Save”. Now look at the same form, when the trigger is “Save”

Form OnSave

As you can see, the “Salutation” field is not set immediately. It will be set only

  1. When the form is saved OR
  2. When the “Gender” field is changed

One more thing: Every time when you deactivate and reactivate a business rule, new process triggers records are created, and so you have to get the correct ids when you update the “event” attribute.

I hope you can now understand the internals of business rule and how to use process trigger to control its behaviour.

References:

  1. TechNet: Create and edit business rules
  2. MSDN – Create or edit how business rules are initiated

 

 

Restricting the customer lookup

EDIT (04/07/2017): After testing this in 8.2.1, I have simplified the code as getLookupTypes and setLookupTypes can be used straight from the lookup itself, instead of going through getLookupDataAttribute.

First off, I have tested this only in CRMOnline on 8.1.0 and 8.2.1 so this might not work for you if you are are in older version. You can now easily restrict the default entity of the lookup, if it links to multiple types. e.g. customer lookup. The script below is for attribute of type “customer”, but you can follow a similar approach for “partylist” as well.

These new functions are undocumented, so they are technically unsupported. So, use this at your own risk. In the script below, I am restricting a field with schemaname “customerid” and of type “customer” to “contact” entity only.

JavaScript

var lookup = Xrm.Page.getAttribute('customerid');

//check if multiple type dropdowns enabled for this lookup and it is not a partylist. For partylist we might want to select an account and a contact
if (lookup.getLookupTypes().length > 1
     && !lookup.getIsPartyList()) {
    lookup.setLookupTypes(['contact']);
}

Screenshots

LookupLookup Search Customer

You can use this script during form load and easily restrict the lookup type.

CRM2015 Update 1 – Grid methods

CRM2015 Update 1 has finally introduced the capability to manipulate/access grids in a supported way. The API methods are documented in https://msdn.microsoft.com/en-us/library/dn932126.aspx. However not all methods are documented. These are the additional methods that you can use, but not documented in msdn. The assumption is the subgrid is displaying the contact entity records.

Object GridRow Collection
Method get
Sample Usage
Xrm.Page
.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)
Description Gets the row at the specified index in the grid
Object  GridRow Collection
Method  getByFilter
Sample Usage
Xrm.Page
.getControl("GridControlName")
.getGrid()
.getRows()
.getByFilter(function(r){
return r.getData()
.getEntity()
.getAttributes()
.getByName('fullname')
.getValue() === 'Max Power';
})
Description  Returns only the rows that have fullname attribute = Max Power
Object  GridRow Collection
Method  getFirst
Sample Usage
Xrm.Page
.getControl("GridControlName")
.getGrid().getRows()
.getByFilter(function(r){
return r.getData()
.getEntity()
.getAttributes()
.getByName('fullname')
.getValue() === 'Max Power';
})
Description  Returns only the 1st row that has fullname attribute = Max Power
Object  GridRow
Method  getKey
Sample Usage
Xrm.Page.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)
.getKey()
Description  Return the primary key of the 1st row
Object  GridRow
Method  getId
Sample Usage
Xrm.Page
.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)
.getData()
.getEntity()
.getId()
Description  Return the primary key of the 1st row
Object  Attribute
Method  getValue
Sample Usage
Xrm.Page.getControl("GridControlName")
.getGrid()
.getRows()
.get(0)
.getData()
.getEntity()
.getAttributes()
.getByName("fullname")
.getValue()
Description  Gets the value for the fullname attribute from the first row in the grid. If this attribute is not a grid column, this method will error as getByName will return null
Object  Grid
Method  openAssociatedGrid
Sample Usage
Xrm.Page.getControl("GridControlName")
.openAssociatedGrid()
Description  Opens the associated view, if it is in the navigation pane

Executing QuickFind using CRM SDK

Global search in CRM for Tablets executes Quickfind view across the entities specified in the System Settings.

Internally the tablet client uses ExecuteQuickFindRequest to perform this quick find search. We can perform the same request using OrganizationRequest. Let’s look at the code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Tooling.Connector;
using System.Net;

namespace ExecuteQuickFind
{
    class Program
    {
        static void Main(string[] args)
        {
            var crmConnection = new CrmConnectionHelper(new NetworkCredential("[username]","[password]","CRM"),AuthenticationType.AD, "crm1","80","Contoso");
            if(crmConnection.IsReady)
            {
                var executeQuickFindRequest = new OrganizationRequest("ExecuteQuickFind");
                executeQuickFindRequest.Parameters = new ParameterCollection();
                var entities = new List<string> { "contact", "lead","opportunity","systemuser","competitor","activitypointer", "incident" };
                //specify search term
                executeQuickFindRequest.Parameters.Add(new KeyValuePair<string object="">("SearchText","maria"));
                //will cause serialisation exception if we don't convert to array
                executeQuickFindRequest.Parameters.Add(new KeyValuePair<string object="">("EntityNames", entities.ToArray()));
                
                var executeQuickFindResponse = crmConnection.OrganizationServiceProxy.Execute(executeQuickFindRequest);
                var result = executeQuickFindResponse.Results.FirstOrDefault();
                if (executeQuickFindResponse.Results.Any())
                {
                    var quickFindResults = result.Value as QuickFindResultCollection;

                    if (quickFindResults != null)
                    {
                        foreach (var quickFindResult in quickFindResults)
                        {
                            if (quickFindResult.ErrorCode != 0)
                            {
                                Console.WriteLine("Quickfind for {0} errored with code {1}",
                                                  quickFindResult.Data.EntityName,
                                                  quickFindResult.ErrorCode);
                                continue;
                            }
                            Console.WriteLine("***Entity {0} returned {1} record(s)***", quickFindResult.Data.EntityName,
                                              quickFindResult.Data.Entities.Count);
                            foreach (var entityRow in quickFindResult.Data.Entities)
                            {
                                foreach (
                                    var attribute in
                                        entityRow.Attributes.Where(
                                            attribute => !entityRow.FormattedValues.Any(x => x.Key == attribute.Key)))
                                {
                                    Console.WriteLine("{0} = {1}", attribute.Key, ExtractValue(attribute.Value));
                                }
                                foreach (var formattedAttributes in entityRow.FormattedValues)
                                {
                                    Console.WriteLine("Formatted: {0} = {1}", formattedAttributes.Key,
                                                      formattedAttributes.Value);
                                }
                                Console.WriteLine("-----------------------------------------");
                            }
                        }
                    }
                }               
            }
        }

        private static object ExtractValue(object attributeValue)
        {
            var attributeType = attributeValue.GetType().Name;
            object returnValue = attributeValue;
            switch (attributeType)
            {
                case "OptionSetValue":
                    returnValue = ((OptionSetValue) attributeValue).Value;
                    break;
                case "EntityReference":
                    returnValue = ((EntityReference)attributeValue).Name;
                    break;
                case "Money":
                    returnValue = ((Money)attributeValue).Value;
                    break;
            }
            return returnValue;
        }
    }
}

Here is the result after executing this code.

The maximum number of entities you can specify in the EntityNames parameter for this ExecuteQuickFindRequest is 10.