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

 

 

 

Business Solutions MVP – 2017

I was pleasantly surprised to see a MVP award notification email, when I woke up today morning.

MVP Email.png

I usually share stuff on Twitter pretty quickly, but I haven’t done so with this announcement, as I wanted to credit all the people who have not only helped me, but the whole CRM community with their insightful posts and awesome tools.

As a developer I really look up to these people, who have made some significant open source contributions and continue to:

  • XrmToolBox team (Alexey Shytikov, Daryl LaBar, Jonas Rapp, Tanguy Touzard)
  • Scott Durow
  • Jason Lattimer
  • Guido Preite
  • Yaniv Arditi
  • Lucas Alexander

The MVP program has also been updated. These are the key changes:

  1. New MVPs will be awarded every month (It was quarterly earlier)
  2. All renewals will happen on July

I think these changes are great, as this means more people will be able to come into the program quicker.

Reference:

https://blogs.msdn.microsoft.com/stevengu/2017/02/01/continuing-the-evolution-of-the-mvp-award/

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

Gotcha: DateTimeKind and FetchXml

I was going through the FetchXML schema and found these two interesting attributes. The first one is “utc-offset” on the “fetch” node.

utcoffset

The other one is “usertimezone” on the “attribute” node.

usertimezone

Both these attributes give an impression, that it is possible to return datetime attributes in a different timezone instead of UTC, but in reality they don’t seem to do anything.

The only way to return datetime in the user’s local timezone, is by using the deprecated ExecuteFetchRequest. I will demonstrate this with XrmToolBox. This is my simple fetch query.

fetchxml-with-datetime

Below is the raw fetch result

fetch-xml-response-raw

Below is the table grid result

fetch-xml-response-retrievemultiple

I execute the FetchXML using the FetchXML Builder tool. As you can see, the datetime values are different. This is because when you use “RetrieveMultiple” to execute the fetch query, the datetime returned is always UTC and has to be converted to the local timezone. ExecuteFetchRequest is now deprecated and RetrieveMultiple is the recommended way to execute fetchxml. These two points really puzzle me:

  1. Why user-timezone and utc-offset don’t seem to do anything. Are they internally used?
  2. Why MS decided to change the datetime behaviour

So, this is a good point to remember when you are executing fetchxml using “FetchXML Tester” or have set the Result view to Raw fetch result in “FetchXML Builder” or using a mixture of ExecuteFetchRequest and RetrieveMultiple in your code.

References:

  1. ExecuteFetchRequest class
  2. XrmToolBox Issue #326

 

Bookmarklet: Copy and Paste Lookup

Lookups (including partylist and customer) cannot be copy pasted. I have encountered couple of scenarios where I needed this capability. I have developed these bookmarklets to solve this issue.

Copy Lookup

Select and the drag the code below to the bookmark bar.

javascript:(function(){let contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){let Xrm=contentPanels[0].contentWindow.Xrm;let currentControl=Xrm.Page.ui.getCurrentControl();if(currentControl&¤tControl.getControlType()==='lookup'){let currentLookup=currentControl.getAttribute().getValue();if(currentLookup){let serialisedLookupValue=JSON.stringify(currentLookup.map(x=>{let c={};({id:c.id,name:c.name,type:c.type,typename:c.typename,entityType:c.entityType}=x);return c;}));sessionStorage.setItem('ryr_serialisedLookup',serialisedLookupValue);alert('Lookup copied. Ready to paste');}}else{alert('No field has been selected or the currently selected field is not a lookup');}}else{alert('Entity form not detected');}})();void 0;

Source:

(function () {
	let contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		let Xrm = contentPanels[0].contentWindow.Xrm;
		let currentControl = Xrm.Page.ui.getCurrentControl();
		if (currentControl && currentControl.getControlType() === 'lookup') {
			let currentLookup = currentControl.getAttribute().getValue();
			if (currentLookup) {
				let serialisedLookupValue = JSON.stringify(
						currentLookup.map(x =  > {
								let c = {};
								({
									id : c.id,
									name : c.name,
									type : c.type,
									typename : c.typename,
									entityType : c.entityType
								}
										 = x);
								return c;
							}));
				sessionStorage.setItem('ryr_serialisedLookup', serialisedLookupValue);
				alert('Lookup copied. Ready to paste');
			}
		} else {
			alert('No field has been selected or the currently selected field is not a lookup');
		}
	} else {
		alert('Entity form not detected');
	}
})();

Paste Lookup

Select and the drag the code below to the bookmark bar.

javascript:(function(){let contentPanels=Array.from(document.querySelectorAll('iframe')).filter(function(d){return d.style.visibility!=='hidden'});if(contentPanels&&contentPanels.length>0){let Xrm=contentPanels[0].contentWindow.Xrm;let currentControl=Xrm.Page.ui.getCurrentControl();if(currentControl&¤tControl.getControlType()==='lookup'){let currentLookup=currentControl.getAttribute();let copiedLookupValue=sessionStorage.getItem('ryr_serialisedLookup');if(copiedLookupValue){currentLookup.setValue(JSON.parse(copiedLookupValue));} else{alert('Please select a lookup to copy first before pasting');}}else{alert('No field has been selected or the currently selected field is not a lookup');}}else{alert('Entity form not detected');}})();void 0;

Source:

(function () {
	let contentPanels = Array.from(document.querySelectorAll('iframe')).filter(function (d) {
			return d.style.visibility !== 'hidden'
		});
	if (contentPanels && contentPanels.length > 0) {
		let Xrm = contentPanels[0].contentWindow.Xrm;
		let currentControl = Xrm.Page.ui.getCurrentControl();
		if (currentControl && currentControl.getControlType() === 'lookup') {
			let currentLookup = currentControl.getAttribute();
			let copiedLookupValue = sessionStorage.getItem('ryr_serialisedLookup');
			if(copiedLookupValue){
				currentLookup.setValue(JSON.parse(copiedLookupValue));
			}
			else{
				alert('Please select a lookup to copy first before pasting');
			}
		} else {
			alert('No field has been selected or the currently selected field is not a lookup');
		}
	} else {
		alert('Entity form not detected');
	}
})();

Instructions

  1. Select the lookup field to be copied. The lookup will usually be highlighted with a blue background when it is selectedlookup-selected
  2. Run the Copy Lookup bookmarklet. You will get a alert message confirming that the lookup has been copied
  3. Select the lookup field that is the target of the paste. The lookup has to be of the same type i.e. if the copied lookup was a contact lookup, the target lookup also has to be a contact lookup
  4. Run the Paste Lookup bookmarklet
  5. Save the record

I have tested these bookmarklets only in Chrome 53 and Firefox 50.

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