Bookmarklet: Open view in new window

Drag the below bookmarklet to the favorites bar in your browser, to quickly open the current view being displayed, in a new window. You can then probably bookmark the view Url, to assist with your daily housekeeping tasks. The current way the get the view url is Email a Link -> Of Current view from the command bar and then copy the url from the resulting Outlook window.

ViewCommandbar

This bookmarket will save you that 3 seconds to do that.

javascript:if(frames&&frames[0].$P_CRM&&frames[0].$P_CRM('#crmGrid')){var grid=frames[0].$P_CRM('#crmGrid')[0].control;if(grid){window.open(frames[0].Xrm.Page.context.getClientUrl()+'/main.aspx?etc='+grid.GetParameter('otc')+'&pagetype=entitylist&viewid='+grid.GetParameter('viewid')+'&viewtype='+grid.GetParameter('viewtype'),'_blank');}else{alert('Unable to find grid');}}else{alert('Unable to find grid');} void 0;

The source for this is as below

if (frames && frames[0].$P_CRM && frames[0].$P_CRM('#crmGrid')) {
	var grid = frames[0].$P_CRM('#crmGrid')[0].control;
	if (grid) {
		window.open(frames[0].Xrm.Page.context.getClientUrl() + '/main.aspx?etc=' + grid.GetParameter('otc') + '&pagetype=entitylist&viewid=' + grid.GetParameter('viewid') + '&viewtype=' + grid.GetParameter('viewtype'), '_blank');
	} else {
		alert('Unable to find grid');
	}
} else {
	alert('Unable to find grid');
}

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

Form Script Bundling and Minification

Ever since form script loading was made async, I have tried different ways to ensure that form scripts will be loaded in the sequence I want, and not how CRM wants it to be, a.k.a random. CRM2015 Update 1 has introduced Turbo Forms. Turbo Forms are supposed to drastically improve form render time. The problem of managing script dependencies, is still left to the developer. You basically have two options:

  1. In build stage, bundle and minify the scripts in the order of dependencies and use this in the form.
  2. Write self contained scripts, without any dependencies.

I tried out an approach using Plugin and couple of entities to try solve this. If you would rather read the code, you can download the source from https://github.com/rajyraman/DynamicScriptBundling. I have also included the managed and unmanaged solutions in the repo.

Components

1. Form Load sequence entity, that stores the entity name and scripts that are to be loaded
 2. A blank javascript webresource, whose name is in this pattern: [entityname].crmform.min.js
 3. A plugin the runs on post-RetrieveMultiple on webresource entity

How it is wired up

[entityname].crmform.min.js is added to the form, that requires bunding and minification. This script just contains a comment, and nothing else.

Create a Form Load Sequence record, that specifies the entity name and the scripts that loaded be minified for this entity.

Register the plugin on post RetrieveMultiple of webresource entity.

How it works

If you have read the plugin code already, you already know. But you haven’t here is how it works:
The plugin retrieves the Query key on the PluginExecutionContext’s InputParameter that contains the QueryExpression object. It then checks, if there is a condition on this QueryExpression with name like crmform.min.js. If so, it retrieves the correct Form Load sequence record for the current entity.

Using the script sequence specified, it also retrieves the script webresources, concatenates and minifies them. This concatenated script is then used to update the OutputParameter’s BusinessEntityCollection. The plugin also checks if the javascript webresources specified in the form load sequence entity, actually exists. It displays an error, if it doesn’t.

ExecutionContext for RetrieveMultiple on webresource – Post Stage

  <Configuration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorDurationInMilliseconds>2</ConstructorDurationInMilliseconds>
  <ConstructorException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ConstructorStartTime>2015-05-22T14:04:15.5180427Z</ConstructorStartTime>
  <Context>
    <z:anyType i:type="PluginExecutionContext" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <BusinessUnitId>bf0d0d94-ddc9-e411-80db-c4346bad5414</BusinessUnitId>
      <CorrelationId>b668ab80-1dda-4676-8615-8d82e9ea0ff0</CorrelationId>
      <Depth>1</Depth>
      <InitiatingUserId>80151b28-fb9a-4d38-a920-d8f4d33ffcc2</InitiatingUserId>
      <InputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>Query</b:key>
          <b:value i:type="a:QueryExpression">
            <a:ColumnSet>
              <a:AllColumns>false</a:AllColumns>
              <a:Columns xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                <c:string>webresourceid</c:string>
                <c:string>name</c:string>
                <c:string>content</c:string>
                <c:string>webresourcetype</c:string>
                <c:string>silverlightversion</c:string>
              </a:Columns>
            </a:ColumnSet>
            <a:Criteria>
              <a:Conditions>
                <a:ConditionExpression>
                  <a:AttributeName>name</a:AttributeName>
                  <a:Operator>Equal</a:Operator>
                  <a:Values xmlns:c="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
                    <c:anyType i:type="d:string" xmlns:d="http://www.w3.org/2001/XMLSchema">ryr_formexperiment.crmform.min.js</c:anyType>
                  </a:Values>
                  <a:EntityName i:nil="true" />
                </a:ConditionExpression>
              </a:Conditions>
              <a:FilterOperator>And</a:FilterOperator>
              <a:Filters />
            </a:Criteria>
            <a:Distinct>false</a:Distinct>
            <a:EntityName>webresource</a:EntityName>
            <a:LinkEntities />
            <a:Orders />
            <a:PageInfo>
              <a:Count>0</a:Count>
              <a:PageNumber>0</a:PageNumber>
              <a:PagingCookie i:nil="true" />
              <a:ReturnTotalRecordCount>false</a:ReturnTotalRecordCount>
            </a:PageInfo>
            <a:NoLock>false</a:NoLock>
          </b:value>
        </a:KeyValuePairOfstringanyType>
      </InputParameters>
      <IsExecutingOffline>false</IsExecutingOffline>
      <IsInTransaction>false</IsInTransaction>
      <IsOfflinePlayback>false</IsOfflinePlayback>
      <IsolationMode>2</IsolationMode>
      <MessageName>RetrieveMultiple</MessageName>
      <Mode>0</Mode>
      <OperationCreatedOn>2015-05-22T14:04:15.0754261Z</OperationCreatedOn>
      <OperationId>00000000-0000-0000-0000-000000000000</OperationId>
      <OrganizationId>32d64867-9081-49c2-8196-6304db7d47e1</OrganizationId>
      <OrganizationName>Contoso</OrganizationName>
      <OutputParameters xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
        <a:KeyValuePairOfstringanyType>
          <b:key>BusinessEntityCollection</b:key>
          <b:value i:type="a:EntityCollection">
            <a:Entities>
              <a:Entity>
                <a:Attributes>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>webresourceid</b:key>
                    <b:value i:type="z:guid">02fe0b47-8800-e511-80ef-c4346bada558</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>name</b:key>
                    <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">ryr_formexperiment.crmform.min.js</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>content</b:key>
                    <b:value i:type="c:string" xmlns:c="http://www.w3.org/2001/XMLSchema">Ly9yeXJfZm9ybWV4cGVyaW1lbnQuY3JtZm9ybS5taW4uanM=</b:value>
                  </a:KeyValuePairOfstringanyType>
                  <a:KeyValuePairOfstringanyType>
                    <b:key>webresourcetype</b:key>
                    <b:value i:type="a:OptionSetValue">
                      <a:Value>3</a:Value>
                    </b:value>
                  </a:KeyValuePairOfstringanyType>
                </a:Attributes>
                <a:EntityState i:nil="true" />
                <a:FormattedValues>
                  <a:KeyValuePairOfstringstring>
                    <b:key>webresourcetype</b:key>
                    <b:value>Script (JScript)</b:value>
                  </a:KeyValuePairOfstringstring>
                </a:FormattedValues>
                <a:Id>02fe0b47-8800-e511-80ef-c4346bada558</a:Id>
                <a:KeyAttributes xmlns:c="http://schemas.microsoft.com/xrm/7.1/Contracts" />
                <a:LogicalName>webresource</a:LogicalName>
                <a:RelatedEntities />
                <a:RowVersion>1308124</a:RowVersion>
              </a:Entity>
            </a:Entities>
            <a:EntityName>webresource</a:EntityName>
            <a:MinActiveRowVersion>-1</a:MinActiveRowVersion>
            <a:MoreRecords>false</a:MoreRecords>
            <a:PagingCookie><cookie page="1"><webresourceid last="{02FE0B47-8800-E511-80EF-C4346BADA558}" first="{02FE0B47-8800-E511-80EF-C4346BADA558}" /></cookie></a:PagingCookie>
            <a:TotalRecordCount>-1</a:TotalRecordCount>
            <a:TotalRecordCountLimitExceeded>false</a:TotalRecordCountLimitExceeded>
          </b:value>
        </a:KeyValuePairOfstringanyType>
      </OutputParameters>
      <OwningExtension xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts">
        <a:Id>34846b5c-8b00-e511-8101-c4346bade5b0</a:Id>
        <a:KeyAttributes xmlns:b="http://schemas.microsoft.com/xrm/7.1/Contracts" xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
        <a:LogicalName>sdkmessageprocessingstep</a:LogicalName>
        <a:Name>RYR.Experiments.PreRetrieveMultipleWebResource: RetrieveMultiple of webresource (Profiler)</a:Name>
        <a:RowVersion i:nil="true" />
      </OwningExtension>
      <PostEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PreEntityImages xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <PrimaryEntityId>00000000-0000-0000-0000-000000000000</PrimaryEntityId>
      <PrimaryEntityName>webresource</PrimaryEntityName>
      <RequestId i:nil="true" />
      <SecondaryEntityName>none</SecondaryEntityName>
      <SharedVariables xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.datacontract.org/2004/07/System.Collections.Generic" />
      <UserId>80151b28-fb9a-4d38-a920-d8f4d33ffcc2</UserId>
      <ParentContext i:nil="true" />
      <Stage>40</Stage>
    </z:anyType>
  </Context>
  <ExecutionDurationInMilliseconds>14</ExecutionDurationInMilliseconds>
  <ExecutionException i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <ExecutionStartTime>2015-05-22T14:04:15.5180427Z</ExecutionStartTime>
  <HasServiceEndpointNotificationService>true</HasServiceEndpointNotificationService>
  <IsContextReplay>false</IsContextReplay>
  <IsolationMode>2</IsolationMode>
  <OperationType>Plugin</OperationType>
  <ProfileVersion>1.1</ProfileVersion>
  <ReplayEvents xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <SecureConfiguration i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
  <TypeName>RYR.Experiments.PreRetrieveMultipleWebResource</TypeName>
  <WorkflowInputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowOutputParameters xmlns:a="http://schemas.datacontract.org/2004/07/PluginProfiler.Plugins" />
  <WorkflowStepId i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" />
</Profile>

Performance
There is a bit of overhead, as the plugin has to retrieve and minify the webresources.


Scripts minified at build stage and added to the form

It takes 291ms.

Script dynamically bundled and minified by plugin

It takes 481ms.

Take this numbers, with a pinch of salt, as I found the performance can vary quite a bit depending on the time of the day, as I tested this in CRMOnline. I also tested this with cache disabled.

Failures along the way
I tried these approaches which, didn’t quite workout, but I want to document them for future reference.

  1. Adding a non-existent form script into the Form Experiment‘s FormXml doesn’t work, even though there is plugin to take care of the RetrieveMultiple request. CRM doesn’t allow this to happen. The Javascript webresource has to exist, if you want to add this to a form. Here is what you’ll have to add to the root node of the formxml, to hook up the webresource and associated onsave and onload event handlers for the form
<formLibraries>
 <Library name='[JSWEBRESNAME]' libraryUniqueId='[NEWGUID]' />
</formLibraries>
<events>
 <event name='onload' application='false' active='false'>
  <Handlers>
   <Handler functionName='[FUNCNAME]' libraryName='[JSWEBRESNAME]' handlerUniqueId='[NEWGUID]' enabled='true' parameters='' passExecutionContext='false' />
  </Handlers>
 </event>
 <event name='onsave' application='false' active='false'>
  <Handlers>
   <Handler functionName='[FUNCNAME]' libraryName='[JSWEBRESNAME]' handlerUniqueId='[NEWGUID]' enabled='true' parameters='' passExecutionContext='false' />
  </Handlers>
 </event>
</events>

 

My previous posts on the same subject

  1. https://dreamingincrm.com/2015/01/14/an-alternative-approach-to-loading-form-scripts-in-dynamics-crm/
  2. https://dreamingincrm.com/2014/04/15/using-requirejs-in-crm2013/

References

  1. Scott Durow – Ghost Webresources
  2. Ben Hosk – Turbo Forms
  3. MSDN – Write Code for CRM Forms

Notes Control in CRM 2013 SP1

If you have a note control on a form and the default tab on this note control is Notes, no note records might be displayed, even though there are notes that are related to the record. Once you click the Notes link on the form, you’ll suddenly see notes being populated. This is a bug that has been fixed in 6.1.1.

In order to automatically click the Notes link, I have a small script running in the onLoad event of the form to fix this issue on CRM 2013 SP1. This script is a stop gap measure, till 6.1.1 is applied to the server.

setTimeout("(function() { var t = document.querySelector('.tabLink.active'); if(t) { t.click(); } })();",2000);

This issue doesn’t seem to be happening in CRM2013 SP1 UR1 and CRM2015 and hence this (unsupported) fix is not applicable to these versions.

Business Rules by Form Type

Xrm.Page.ui.getFormType() is used in form script to find out what type of form is currently loaded. Sometimes, we want to apply a certain logic, depending on whether it is a create form or update form. e.g I want to disable some fields, if it is an update form.

If we are using Business Rules, it is not very obvious (at least to me) on how this can be achieved. The answer is quite simple: just check the value of any of these system fields (created, createdon, modifiedby, modifiedon).

Here is a business rule that will trigger only for update form.

Here is the rule for create form.

Here is the result after the rule has run on an existing record

The important thing to remember is: The system field you are checking (in this case createdon), has to be on the form. Otherwise the rule will not fire.

Credits to @BernadoNH for this info.

An alternative approach to loading form scripts in Dynamics CRM

With each browser update, full ES6 support has been getting closer and closer. But this is still sometime away, and transpilers like traceur or 6to5 can help bridge the gap in some areas. One ES6 functionality I am very much interested in, is module. As of today, no browser natively supports this, and I would have to transpile my code to get this functionality.So, I once again would like to use my favorite module loader, requirejs for doing this.

The last time I did this (https://dreamingincrm.com/2014/04/15/using-requirejs-in-crm2013/) I had to use some unsupported tricks to get this working in CRM2013. This time, my approach is to do away with CRM script loading mechanism altogether and use requirejs to load the form scripts. Here is how my resources are organised.

Events.html is the HTML webresource that will be embedded in the entity form. Here is the code for events.html;
Form Events
  <!-- data-main attribute tells require.js to load
  scripts/main.js after require.js loads. --><script src="scripts/require.js" data-main="scripts/main"></script></pre>
<ul id="events"></ul>
<pre>
main.js is the entry point into the form processing code.
(function () {
  var defaultConfig = {
    shim: {
      'lodash': {
        exports: '_'
      }
    },
    deps: ['lodash', 'common', 'ryr_eventform'],
    callback: function() {
      console.log('callback before requirejs has been loaded');
    },
    onError: function(err) {
      console.log(err.requireType);
      if (err.requireType === 'timeout') {
        console.log('modules: ' + err.requireModules);
      }
      throw err;
    }
  };
  defaultConfig.callback = function() {
    console.log('callback after requirejs has been loaded');
  };
  requirejs.config(defaultConfig);
})();
I want the scripts to load in the following order: lodash->common->ryr_eventform. If you use the form area to reference your script, you really don’t have any control over the sequence, as they are loaded async and may not be loaded in the same order you added them in the form (http://www.develop1.net/public/post/Asynchronous-loading-of-JavaScript-Web-Resources-after-U12POLARIS.aspx). Until Microsoft changes this functionality, there are two ways to overcome this issue.

1.) Bundle all your scripts in the order of their dependencies
2.) Check if the dependency has loaded. (See the waitForScript technique in the develop1 link)

I am loading the scripts using requirejs, but the triggering page is an external web resource. This way I can keep this a supported method.

This is common.js, which is required by ryr_eventform.js.

define(['lodash'], function (_) {
    var common = {
      log: function(message) {
        var e = document.createElement("li");
        e.innerHTML = new Date().toString().split(' ')
        .filter(function(d,i){ return i>0 && i<=4})
        .join(' ') +': '+ message;
        document.getElementById('events').appendChild(e);
      }
    };
    common.log('Loading common_script.js');
    //can use lodash, as it is specified as a dependency and should have been loaded
    common.log('Lodash Version: '+_.VERSION);
    return common;
});
This is ryr_eventform.js.
define(['common', 'lodash'], function (common,_) {
 common.log('Loading ryr_eventform.js');
 //can use lodash, as it is specified as a dependency and should have been loaded
 common.log('Lodash Version: '+_.VERSION);
 var Xrm = parent.Xrm;

 var form = {
  onSave: function(context) {
   common.log('Form Save Event');
  },
  onLoad: function() {
   common.log('Form Load Event');
   if(Xrm){
    Xrm.Page.data.entity.addOnSave(this.onSave);
    Xrm.Page.getAttribute('ryr_name').addOnChange(function(context) {
     common.log('Name Change Event: ' + context.getEventSource().getValue());
    });
    }
   else{
    common.log('Web Resource has not been embedded inside a CRM form');
   }
  }
 };

 form.onLoad();
 return form;
});
Here is how the form looks in the design mode.
I have not added any scripts to the form.

Since requirejs will start loading the scripts, you don’t need to worry about this.

Lets start looking at some form events now and how the script behaves.
Form Load
Name field changed
 As you, can see a script can do exactly the same things, even though it has not been been loaded through the CRM form script loading mechanism. These are are two key things that help to achieve this.
1.) The webresource folder structure
2.) Referencing Xrm object from webresource using parent.Xrm

The impetus for this post is this: I have got “The form has changed. Would you like to save your changes” dialog more than a few times and I have no idea what is the reason for this dialog.

If the change has made by a script, I have no way of knowing what the change was, and which script triggered this, unless I have added some console.log message the scripts. This is not possible if I can’t change the script. You could live edit the script using the DevTools, but I don’t want to do that.

The disadvantages of this techique, that I can see are

  1. html webresource has to be added to the form
  2. Tablet support
If CRM Client API exposes some sort of event listening capability, this would help the devs to listen to certain events like form save, form load, field onchange from the devtools console and figure out what is happening with the form, without using the debugger step through.

CRM itself, uses custom events and listeners internally, to figure out what scripts to execute for a particular event. But this functionality is not exposed externally for everyone to use. Until this is made available down the line, into some sort of Client API – Dev Mode, I can use this to control the form script loading process and audit of form events.

Copy Record Id of a row from Advanced Find

I have recently started using bookmarklets to improve productivity during CRM Development. There are plenty of bookmarklets that I use, and of these I quite frequently use these:

  1. Copy Record Id (http://blog.sonomapartners.com/2014/01/crm-2013-javascript-bookmark-series-part-1.html)
  2. Open Advanced Find (http://www.magnetismsolutions.com.au/blog/paulnieuwelaar/2014/07/24/crm-2013-open-advanced-find-from-anywhere-with-bookmarklet)
  3. Open Default Solution (http://www.magnetismsolutions.com.au/blog/paulnieuwelaar/2014/07/27/customize-and-publish-from-crm-2013-forms-with-bookmarklets)

Inorder to use the Copy Record Id bookmarket you’ll have to be in the record form. I found this inconvinient when I was viewing the results from Advanced Find. You can bookmarklet the below script to quickly extract the primary key of the selected row in the Advanced Find resultset.

javascript:var contentFrame=document.getElementById('contentIFrame0'),isError=false;if(contentFrame){var resultFrame=contentFrame.contentWindow.document.getElementById('resultFrame');if(resultFrame&amp;&amp;resultFrame.contentWindow){var selectedRow=resultFrame.contentWindow.document.querySelector('.ms-crm-List-SelectedRow');if(selectedRow){window.prompt('Copy to clipboard: Ctrl+C, Enter',selectedRow.getAttribute('oid'));}<br />else{alert('Please select a row to get the id');}}else{isError=true;}}else{isError=true;}<br />if(isError){alert('Unable to locate result frame to extract rowid');}<br />void 0;<br />

Here is the how it looks when you run the code on a row in the advanced find result.

I have tested this in the latest version of Firefox (33) and Chrome (38) and it works.

Retrieving Server Datetime using Javascript

I recently saw a forum post about retrieving server datetime using Javascript. My initial impressions were
1.) It is a bad idea to do this
2.) It is not possible

I attended DDDMelbourne recently and there was a talk about why bad ideas are the best ideas and so I set about trying to get this working. This looked like it could be done using Actions.

Problem No 1:
The simplest approach would be create a new Action with a Datetime output parameter and set this to Process’ Execution Time. I did do this and here is when I found this first issue. CrmDatetime is STILL present in CRM 2013. Here is what I got when I executed the Action from the console.

Create a custom activity
This meant that I have to set the output Datetime parameter using a custom activity. This is the custom activity code. It is pretty simple. All it does it set the various output parameters of the Action.

namespace ServerTimeActivity
{
    using System;
    using System.Activities;
    using System.ServiceModel;
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Workflow;</pre>
public sealed class RetrieveServerTimeActivity : CodeActivity
{
///
/// Executes the workflow activity.
///

/// The execution context.
protected override void Execute(CodeActivityContext executionContext)
{
// Create the tracing service
ITracingService tracingService = executionContext.GetExtension();

if (tracingService == null)
{
throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
}

tracingService.Trace("Entered RetrieveServerTimeActivity.Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",
executionContext.ActivityInstanceId,
executionContext.WorkflowInstanceId);

// Create the context
IWorkflowContext context = executionContext.GetExtension();

if (context == null)
{
throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
}

tracingService.Trace("RetrieveServerTimeActivity.Execute(), Correlation Id: {0}, Initiating User: {1}",
context.CorrelationId,
context.InitiatingUserId);

IOrganizationServiceFactory serviceFactory = executionContext.GetExtension();
IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

try
{
//TimeZoneInfo.ClearCachedData();
//System.Globalization.CultureInfo.CurrentCulture.ClearCachedData();

ServerDateTime.Set(executionContext, DateTime.Now);
ServerDateTimeString.Set(executionContext, DateTime.Now.ToString(System.Globalization.CultureInfo.CurrentCulture));
ServerTimezoneUtcOffset.Set(executionContext, TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes);
ServerTimezone.Set(executionContext, TimeZoneInfo.Local.ToString());
}
catch (FaultException e)
{
ServerDateTime.Set(executionContext, e.StackTrace);
tracingService.Trace("Exception: {0}", e.ToString());

// Handle the exception.
throw;
}

tracingService.Trace("Exiting RetrieveServerTimeActivity.Execute(), Correlation Id: {0}", context.CorrelationId);
}

[Output("Server Datetime")]
public OutArgument ServerDateTime { get; set; }

[Output("Server Datetime String")]
public OutArgument ServerDateTimeString { get; set; }

[Output("Server Timezone")]
public OutArgument ServerTimezone { get; set; }

[Output("Server Timezone UTC Offset")]
public OutArgument ServerTimezoneUtcOffset { get; set; }

[Output("Exception Stack Trace")]
public OutArgument ExceptionStackTrace { get; set; }
}
}

The next step is to use this custom activity inside the Action. Here is what my Action looks like

Once this done we have generate the JS code for this action using the Sdk.Soap.js Action Message Generator. Generate the JS code for all the actions by executing the Sdk.SoapActionMessageGenerator.exe.

In order to use this in a form we need two scripts

1.) Sdk.Soap.js
2.) Sdk.new_ServerDateTime.min.js -> This is the name of my action

Add these scripts as webresource and reference them in the form or ribbon button that will require this functionality. Be mindful of the load sequence, as this not guaranteed due to the async nature of the script load. Follow this excellent tutorial from CRM MVP Scott Durow if you want to manage this issue, or refer to my earlier post about using require.js.

This is the client side JS you can use.

(function() {
    Sdk.Async.execute(new Sdk.new_ServerDateTimeRequest(),function(response) {
        console.log('Datetime String ' + response.getServerDatetimeString());
        console.log('Server UtcOffset ' + response.getServerUtcOffset());
        console.log('Server Datetime ' + response.getServerDateTime());
        console.log('Server Timezone String ' + response.getServerTimezone());</pre>
}, function (message) { alert(message); });
})();

I ran the script from Chrome DevTools console and here is how the output looks like.

Observations
1.) The CRMOnline server that I am using is in APAC, but the server is in UTC timezone
2.) If the output parameter of an action is DateTime, it always returns local time and not the server time
3.) If the Datetime is converted to string then the server time is returned
4.) The culture on the CRMOnline server is en-US
5.) Assign value will throw an exception if the value being assigned is null

I hope this was helpful in understanding how awesome Actions and Sdk.Soap.js library are.

Messaging with PubNub

Users can concurrently work on the same record in Dynamics CRM and simultaneously make changes. This can cause some changes to be overwritten or missed. The users also have no way of knowing who else is working on a record, when they open it. To address these issue, we will use PubNub as a secure message transportation platform to keep users notified of any changes to record that they are currently viewing.
1.)    Signup for a free account at http://www.pubnub.com. For this proof of concept, we can use the free sandbox account.
2.)    Login to the admin portal and note down the keys.
3.)    We will be using requirejs to load the dependent scripts. There are two scripts to load: the form script and the pubnub script. Create new web resources to store these scripts.

4.)  We will be using the main.js to start the loading process. This is the script that has to be added to the form along with requirejs.

Below is the sourcecode for main.js, the loading script. We are loading pubnub from CDN. The local webresource is only used for fallback, if there is any issue with the CDN.

// <reference path="require.js">
(function () {
    var webResourceVersionNumber = '';
    //get the url for the script, so that we can extract the guid to prefix
    [].forEach.call(document.querySelectorAll('script'), function (element) {
        if (element.src.indexOf('main.js') > -1) {
            webResourceVersionNumber = element.src;
        }
    });
    webResourceVersionNumber = webResourceVersionNumber.replace(Xrm.Page.context.getClientUrl(), '').substr(1, 24);
    var defaultConfig = {
        //could also use undocumented WEB_RESOURCE_ORG_VERSION_NUMBER
        baseUrl: '/' + webResourceVersionNumber + '/WebResources/scripts_/form',
        shim: {
            'pubnub': {
                exports: 'PUBNUB'
            }
        },
        deps: ['pubnub', 'form_script'],
        callback: function () {
            console.log('callback before requirejs has been loaded');
        },
        paths: {
            pubnub: ['https://cdn.pubnub.com/pubnub.min', '../library/pubnub.min']
        },
        onError: function (err) {
            console.log(err.requireType);
            if (err.requireType === 'timeout') {
                console.log('modules: ' + err.requireModules);
            }
            throw err;
        }
    };
    if (!window['require']) {
        window['require'] = defaultConfig;
    } else {
        defaultConfig.callback = function () {
            console.log('callback after requirejs has been loaded');
        };
        require.config(defaultConfig);
    }
})();
Below is the sourcecode for form_script.js.Here is what the form displays when another user has opened the same record.
// <reference path="XrmPage-vsdoc.js">
define(['pubnub'], function(PUBNUB) {
        var pubnub = PUBNUB.secure({
            publish_key: '<PUBLISH KEY><publish key="">',
            subscribe_key: '<SUBSCRIBE KEY><subscribe key="">',
            ssl: true,
            cipher_key: '<CIPHER KEY><cipher key="">'
        }),
        pageContext = Xrm.Page.context,
        entity = Xrm.Page.data.entity,
        userName = pageContext.getUserName(),
        entityName = entity.getEntityName(),
        entityId = entity.getId(),
        userId = pageContext.getUserId(),
        FormState = {
            OPEN: 'opened',
            CLOSE: 'updated and closed',
            UPDATE: 'updated'
        },
        FormSaveType = {
            SAVE: 1,
            SAVEANDCLOSE: 2,
            SAVEANDNEW: 59,
            AUTOSAVE: 70
        },
        FormType = {
            CREATE: 1,
            UPDATE: 2,
            READONLY: 3,
            DISABLED: 4,
            QUICKCREATE: 5,
            BULKEDIT: 6,
            READOPTIMISED: 11
        };
    
        if (Xrm.Page.ui.getFormType() === FormType.UPDATE) {
            pubnub.subscribe({
                channel: "form_events",
                message: function (m) {
                    if (m.userId !== userId) {
                        var now = new Date();
                        var message = now.toLocaleString('en-GB') + ': ' + m.userName + ' ' + m.operation + ' this record';
                        Xrm.Page.ui.setFormNotification(message, 'INFO');
                        if (m.operation === FormState.UPDATE) {
                            Xrm.Utility.confirmDialog('This form has been updated by ' + m.userName + ' at ' + now.toLocaleString('en-GB') + '. Do you want to reload the form to see the latest changes?',
                                function() {
                                    Xrm.Page.data.refresh(false);
                                });
                        }
                    }
                }
            });
            pubnub.publish({
                channel: "form_events",
                message: {
                    userName: userName,
                    userId: userId,
                    entityName: entityName,
                    entityId: entityId,
                    operation: FormState.OPEN,
                    uuid: notifications.uuid
                }
            });
            Xrm.Page.data.entity.addOnSave(function onSave(context) {
                pubnub.publish({
                    channel: "form_events",
                    message: {
                        userName: userName,
                        userId: userId,
                        entityName: entityName,
                        entityId: entityId,
                        operation: FormState.UPDATE,
                        uuid: notifications.uuid
                    }
                });
            });
        }
});

Here is what the form displays when another user has opened the same record.

 

Here is what that form displays when another user has made changes and saved the record.