Quick Tip: Launch URL for Dynamics 365

Today I discovered by accident, an useful setting in Office 365 App launcher. Here is how the Office 365 App launcher looks like

App Launcher.png

This UI is shown when you click the launcher icon next to “Dynamics 365”. You can set an URL to open when you click this icon, instead of navigating to the default “home.dynamics.com”. You do this by clicking the gear icon on the top right and choosing “Preferences”.

Preferences.png

Set Url.png

Once this is set, you can head straight to the normal CRM Dynamics 365 area, instead of “home.dynamics.com” when you click the Dynamics 365 icon in the launcher.

 

 

Bug: Branched Business Process Flow

Branched Business Process Flow was introduced in CRM 2015. I encountered a bug in BPF today, which took a day and a half to figure out. I am posting the scenario so that it will be beneficial for others who experience the same error. The bug is this:

When a lead is qualified an exception is thrown when:

  1. BPF has branches AND
  2. BPF has condition stages AND
  3. Some condition stages have only one output i.e only Yes or No
  4. Data in the form could lead to a condition block that will result in a dead end

This is the BPF, I created to test this bug.

Test BPF.png

As you can see I have a condition called “Existing account is not null”, and a stage if is true, not no stage if it is false. With this BPF an exception is thrown when the lead is qualified and Existing account is null. Below is the full error in Dynamics 365 Online.

lead-qualify-error

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
	<s:Body>
		<s:Fault>
			<faultcode>s:Client</faultcode>
			<faultstring xml:lang="en-US">An unexpected error occurred.</faultstring>
			<detail>
				<OrganizationServiceFault xmlns="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
					<ActivityId>8cf5f7f2-d08f-4e2d-9374-daf02247e5d0</ActivityId>
					<ErrorCode>-2147220970</ErrorCode>
					<ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic"/>
					<Message>An unexpected error occurred.</Message>
					<Timestamp>2017-02-09T06:30:37.8410512Z</Timestamp>
					<ExceptionSource i:nil="true"/>
					<InnerFault>
						<ActivityId>8cf5f7f2-d08f-4e2d-9374-daf02247e5d0</ActivityId>
						<ErrorCode>-2147220970</ErrorCode>
						<ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic"/>
						<Message>System.ArgumentNullException: Value cannot be null.
Parameter name: g</Message>
						<Timestamp>2017-02-09T06:30:37.8410512Z</Timestamp>
						<ExceptionSource i:nil="true"/>
						<InnerFault i:nil="true"/>
						<OriginalException i:nil="true"/>
						<TraceText i:nil="true"/>
					</InnerFault>
					<OriginalException i:nil="true"/>
					<TraceText i:nil="true"/>
				</OrganizationServiceFault>
			</detail>
		</s:Fault>
	</s:Body>
</s:Envelope>

This is the exception detail in CRM 2015.

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
	<s:Body>
		<s:Fault>
			<faultcode>s:Client</faultcode>
			<faultstring xml:lang="en-AU">An unexpected error occurred.</faultstring>
			<detail>
				<OrganizationServiceFault xmlns="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
					<ErrorCode>-2147220970</ErrorCode>
					<ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
						<KeyValuePairOfstringanyType>
							<a:key>CallStack</a:key>
							<a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">   at Microsoft.Crm.Extensibility.VersionedPluginProxyStepBase.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.PipelineInstrumentationHelper.Execute(Boolean instrumentationEnabled, String stopwatchName, ExecuteWithInstrumentation action)
   at Microsoft.Crm.Extensibility.Pipeline.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.MessageProcessor.&lt;&gt;c__DisplayClass1.&lt;RunStage&gt;b__0()
   at Microsoft.Crm.Extensibility.PipelineInstrumentationHelper.Execute(Boolean instrumentationEnabled, String stopwatchName, ExecuteWithInstrumentation action)
   at Microsoft.Crm.Extensibility.MessageProcessor.RunStage(PipelineExecutionContext context, Int32 pipelineStage)
   at Microsoft.Crm.Extensibility.MessageProcessor.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.InternalMessageDispatcher.Execute(PipelineExecutionContext context)
   at Microsoft.Crm.Extensibility.ExternalMessageDispatcher.ExecuteInternal(IInProcessOrganizationServiceFactory serviceFactory, IPlatformMessageDispatcherFactory dispatcherFactory, String messageName, String requestName, Int32 primaryObjectTypeCode, Int32 secondaryObjectTypeCode, ParameterCollection fields, CorrelationToken correlationToken, CallerOriginToken originToken, UserAuth userAuth, Guid callerId, Guid transactionContextId, Int32 invocationSource, Nullable`1 requestId, Version endpointVersion)
   at Microsoft.Crm.Extensibility.OrganizationSdkServiceInternal.ExecuteRequestRequestWithInstrumentation(OrganizationRequest request, CorrelationToken correlationToken, CallerOriginToken callerOriginToken, WebServiceType serviceType, UserAuth userAuth, Guid targetUserId, OrganizationContext context, Boolean returnResponse, Boolean checkAdminMode, Object operation)
   at Microsoft.Crm.Extensibility.OrganizationSdkServiceInternal.ExecuteRequest(OrganizationRequest request, CorrelationToken correlationToken, CallerOriginToken callerOriginToken, WebServiceType serviceType, UserAuth userAuth, Guid targetUserId, OrganizationContext context, Boolean returnResponse, Boolean checkAdminMode)
   at Microsoft.Crm.Extensibility.OrganizationSdkServiceInternal.ExecuteRequest(OrganizationRequest request, CorrelationToken correlationToken, CallerOriginToken callerOriginToken, WebServiceType serviceType, Boolean checkAdminMode)
   at Microsoft.Crm.Extensibility.OrganizationSdkServiceInternal.Execute(OrganizationRequest request, CorrelationToken correlationToken, CallerOriginToken callerOriginToken, WebServiceType serviceType, Boolean checkAdminMode)</a:value>
						</KeyValuePairOfstringanyType>
					</ErrorDetails>
					<Message>An unexpected error occurred.</Message>
					<Timestamp>2017-02-08T05:33:31.5174084Z</Timestamp>
					<InnerFault>
						<ErrorCode>-2147220970</ErrorCode>
						<ErrorDetails xmlns:a="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
							<KeyValuePairOfstringanyType>
								<a:key>CallStack</a:key>
								<a:value i:type="b:string" xmlns:b="http://www.w3.org/2001/XMLSchema">   at System.Guid..ctor(String g)
   at Microsoft.Crm.ObjectModel.BusinessProcess.WorkflowConditionNextStageEvaluator.GetNextStage(StageStep stage)
   at Microsoft.Crm.ObjectModel.BusinessProcess.ActivePathEnumerator.MoveNext()
   at Microsoft.Crm.ObjectModel.BusinessProcess.ActivePathCalculator.Calculate(String targetEntityName)
   at Microsoft.Crm.Common.ObjectModel.NextProcessStageInformationFinder.CalculateGlobalStageAndTraversedInformation(String firstEntityName, String nextEntityName)
   at Microsoft.Crm.Common.ObjectModel.LeadService.QualifyLead(BusinessEntityMoniker leadId, Boolean createAccount, Boolean createContact, Boolean createOpportunity, BusinessEntityMoniker opportunityCurrencyId, BusinessEntityMoniker opportunityCustomerId, BusinessEntityMoniker sourceCampaignId, Int32 statusCode, ExecutionContext context)</a:value>
							</KeyValuePairOfstringanyType>
						</ErrorDetails>
						<Message>System.ArgumentNullException: Value cannot be null.
Parameter name: g</Message>
						<Timestamp>2017-02-08T05:33:31.5174084Z</Timestamp>
						<InnerFault i:nil="true"/>
						<TraceText i:nil="true"/>
					</InnerFault>
					<TraceText i:nil="true"/>
				</OrganizationServiceFault>
			</detail>
		</s:Fault>
	</s:Body>
</s:Envelope>

The root cause for this seems to beĀ GetNextStage. It seems the method can’t figure out what the next stage should be, when the BPF ends with a condition with only one branch. I haven’t tested other scenarios where this error could be triggered, but I was able to consistently reproduce the error on lead qualify.

Unrelated Note:

It seems business process has a client side caching mechanism. I had to clear cache every time after I made change to the BPF to test this issue.

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

 

 

 

Bug: Notes creation in custom entity

There currently seems to be an issue in Dynamics 365 Online, where you can see notes in area in the custom entity, but cannot create new notes. I could reproduce this issue in this version -> Version 1612 (8.2.0.773) (DB 8.2.0.764)

This is how the notes area looks in a custom entity. There is no header area to create new notes.

missing-header-in-notes

Compare this with the contact entity.

notes-area-contact-entity

As you can see, there is a textarea to create new notes, which seems to be missing in the custom entity. I looked into this using DevTools, and it appears to be display issue, as the DOM elements for creating the notes are still there. I quickly wrote this bookmarklet to temporarily show the create new notes text area in the form.

Sourcecode

(function () {
	let contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		let activityWall = contentPanels[0].contentDocument.querySelector('#notesWall div.header');
		if(activityWall && activityWall.style.display === 'none'){
			activityWall.style.display = ''
		}
	} else {
		alert('Entity form not detected');
	}
})();

Bookmarklet

javascript:(function(){let contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){let activityWall=contentPanels[0].contentDocument.querySelector('#notesWall div.header');if(activityWall&&activityWall.style.display==='none'){activityWall.style.display=''}}else{alert('Entity form not detected');}})();void 0;

This is how the custom form will look, after you run the bookmarklet.

after-bookmarklet-header-restore-in-notes

I am not sure what sets the “display” property to none. When the DOM element is initially created, it is created without the “display” property set and then some event handler appears to be modifying this. I set a DOM breakpoint on “Attributes modifications”, but the breakpoint is not retained when I refresh the page, and hence I am unable to catch the “display: none” being set on “#notesWall div.header”. I spent some time to investigate this from the pretty-printed source, but could not figure it out.

Executing large FetchXML with WebAPI

You can easily execute fetchxml in WebAPI using the “fetchXml” query parameter. But this “GET” method won’t work, if the fetchxml is too big. In this case, you have to use the “POST” method to execute the fetchxml.

Sample Request Header:

Accept: application/json
OData-MaxVersion: 4.0
OData-Version: 4.0
Content-Type: multipart/mixed;boundary=batch_contactfetch

Sample Request Body:

--batch_contactfetch
Content-Type: application/http
Content-Transfer-Encoding: binary

GET https://[CRM URL]/api/data/v8.2/contacts?fetchXml=<fetch count="10" ><entity name="contact" ><attribute name="fullname" /></entity></fetch> HTTP/1.1
Content-Type: application/json
OData-Version: 4.0
OData-MaxVersion: 4.0

--batch_contactfetch--

Sample Code:

var req = new XMLHttpRequest();
req.open("POST", Xrm.Page.context.getClientUrl() + "/api/data/v8.2/$batch", true);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "multipart/mixed;boundary=batch_contactfetch");
req.onreadystatechange = function() {
    if (this.readyState === 4) {
        req.onreadystatechange = null;
        if (this.status === 200) {
            var response = JSON.parse(this.response.substring(this.response.indexOf('{'),this.response.lastIndexOf('}')+1));
			console.log(response.value);
        } else {
            Xrm.Utility.alertDialog(this.statusText);
        }
    }
};

var body = '--batch_contactfetch\n'
body += 'Content-Type: application/http\n'
body += 'Content-Transfer-Encoding: binary\n'
body += '\n'
body += 'GET ' + Xrm.Page.context.getClientUrl()+'/api/data/v8.2/contacts?fetchXml=<fetch count="10" ><entity name="contact" ><attribute name="fullname" /></entity></fetch> HTTP/1.1\n'
body += 'Content-Type: application/json\n'
body += 'OData-Version: 4.0\n'
body += 'OData-MaxVersion: 4.0\n'
body += '\n'
body += '--batch_contactfetch--'

req.send(body);

Request Screenshot

Fetch Post Request.png

Response Screenshot

fetch-post-response

Convert Personal View to System View

When you design a query from the “Advanced Find” window, you can save the query for future use. I call this “Personal View”, but the official name for this is “Saved View”. This view is only visible to the person who saved the view (unless it is shared/reassigned).

As more personal views get created, it becomes an issue during migration, as these are not transported in the solution xml. You can of course use Solution Extender to copy this across, but I would like to do this right from the Advanced Find window. Apart from data migration, another scenario where you might find the need to create a system view from a personal view, is when you are constantly sharing views to a large number of people of group. When you are doing this, it is good time to actually make this a system view.

With these scenarios in mind, I have developed a solution that simplifies the process of creating a system view from a personal view. After installing the managed solution, you will see a new button called “Promote to System View” in the advanced find, when you switch to the “Saved Views” tab.

screenshot

To create a system view from the personal view, simply choose the views that need to be converted, and click the “Promote to System View” button. A message will be displayed after the conversion is complete. Once you refresh the window, you should be able to see the newly created system view(s).

Please log any issues/feedback/feature request in the github repo -> https://github.com/rajyraman/Personal-View-to-System-View/issues

You can download the managed solution from https://github.com/rajyraman/Personal-View-to-System-View/releases

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.