CRM JS webresource best practice for ribbon Commands / Forms

Introduction

As a CRM developer, working with script web resources in forms and ribbons, I have learned a few lessons over the years, and I have seen a wide spread from best practice implementation to the complete unstructured mess.

What I often see when taking over an existing implementation, is the lack of namespacing in the JavaScript webresources. A flat function based code structure, where a myriad of functions are clashed directly into the DOM, is not only hard to read, but increases the chance of overriding existing functionality. For example, some commonly seen function names are often “onLoad”, “onSave”, “validate” or “calculate”. Imagine it you are using two different web resources with conflicting definitions of “onLoad” on the same form, without namespacing; You are headed into trouble!

Another common “mistake” is using the same webresource for form events, and for ribbon commands. This is not a problem in itself, but you should be aware that the scripts will be loaded twice, and that it might make debugging more confusing. While form web resources are loaded as normal included scripts during form initialization, command code for ribbon buttons are loaded as unnamed dynamic script blocks when the ribbon loads. This means that you cannot set a breakpoint in the ribbon code before clicking a button the first time (because the script is not yet loaded). It also means that if you use the same web resource for the form and the ribbon, the ribbon dynamic script will overload the form script, and thus you will loose the ability to debug the loaded form script (The dynamic script block can still be debugged if you can locate it…).

This post is an attempt to share some best practices that can prevent these issues, based on my experience. Code examples are found in the bottom.

General JavaScript development

Switch to TypeScript

Do your coding in TypeScript to have better intellisense and compile time error checking. ReSharper 9+ have TypeScript support, to help you code even better TypeScripts

NB: To really be flying with TypeScripts, you should use third party libraries that have TypeScript definition files. Luckily there are Typescript Definition files for jQuery, AngularJS, XrmServiceToolkit, Xrm.Page API, etc. See more at http://definitelytyped.org/

Follow general best practices

Follow general best practices for JavaScript that also applies to TypeScript to some extent: http://www.w3schools.com/js/js_best_practices.asp

There are also many online resources to improve your TypeScript style and code quality. Read up on the official documentation and guides like https://github.com/panuhorsmalahti/typescript-style-guide

Here are also two good CRM specific links, from JoeCRM:

CRM JavaScript Best Practices

JavaScript Best Practices for Microsoft Dynamics CRM 2015

Handle dependency conflicts

Be aware that CRM and certain managed solutions uses common third party libraries like jQuery. If you are using different versions of these libs, you might run into problems were something become not compatible as the last loaded version overloads the others. jQuery has a noConflict function that can be used to load several versions of jQuery at once, and this is facilitated by dependency injection frameworks like requireJS (http://requirejs.org/)

Optimize your scripts for good performance

If you have a lot of script files, and it is hurting load time, consider bundling and minifying scripts. This can be done with for example Gulp (http://gulpjs.com/)

General CRM Scripting

Namespaces

ALWAYS use namespaces in CRM JavaScript. Especially if you divide your logic in multiple scripts. It will make it a lot easier to maintain and read if you can see exactly what is being referenced. Also, you should introduce namespaces from the start, mostly because it is a pain to change all the references later on.

I prefer to use a hierarchical namespace structure, with the following naming convention:

[Orgname].CRM.[EntityLogicalName].[Area]

For example: Contoso.CRM.Account.Form and Contoso.CRM.Account.Ribbon

I also prefer to have a separate namespace for internal functions and variables, so that the only thing exposed through the main namespace is the functions registered as commands or event handlers in the forms

For example: Contoso.CRM.Account.Form._privateMembers

Prepare for re-use

Avoid having duplicate code in your Form JS and Ribbon JS. Place common code in a third library instead if possible, like Contoso.CRM.Account.Common.js. This will not only make the code re-usable, but also easier to maintain as changes will only have to me made in one place.

Use Virtual Folders for webresources

To make it easier to keep you scripts organized, group them in folders on your development computer, and make the name webresource name reflect that path. This is also a recommended practice from Microsoft (https://msdn.microsoft.com/en-us/library/gg309473.aspx)

For example: A file C:\ContosoCRM\Webresources\JavaScript\Account\AccountForm.js could be registered as “new_/Account/AccountForm.js”, with “new_” being the CRM publisher prefix.

Ribbons / Command bar

Use an editor

Use a ribbon editor like Ribbon Workbench (https://ribbonworkbench.uservoice.com/). There are others, but Ribbon Workbench is the one I have had best experiences with.

Load the actual logic in the form when appropriate

If the command/logic is only to be used on the form ribbon/command bar, consider having the actual logic loaded by the form itself, instead of as dynamic script in the ribbon. This will make debugging easier, and promotes reuse of the logic for form events. Also, making changes to web resources is a lot faster if you don’t have to change the command definition in the ribbon at the same time (i.e. changed function signature).

For commands that should be available on the Entity home page and in related views, all required scripts should be loaded by the ribbon itself.

Debugging Ribbon Commands

If you have trouble debugging the dynamic script blocks, you can try the “Breakpoints in Dynamic JavaScript” feature in Chrome. This is described along with other debugging approaches in this post: http://blogs.msdn.com/b/crm/archive/2015/11/29/debugging-custom-javascript-code-in-crm-using-browser-developer-tools.aspx

Loading dependencies in Ribbons / Command bars

If you need to load other JavaScripts in a ribbon command before executing your function (i.e. jQuery), load them by calling the function “isNaN”. The reason for this is that a javascript action in ribbon XML requires a webresource and a function name. isNaN() is part of the ecma javascript definition, and will therefore always be declared. It will also always execute without errors, and has a very little overhead.

Add a Keyboard Shortcut to your button

I think it is a good idea to add keyboard shortcuts for command bar buttons in forms. I have successfully registered keyboard shortcuts in the form’s onLoad event using this library: http://www.webreference.com/programming/javascript-keyboard-shortcuts/2.html
Add a shortcut hint as a tooltip on the button in the ribbon, so you won’t forget.

Code examples

Ribbon command script (JavaScript)

This example uses a minimal implementation, where the actual logic is in a separate file that is used both by the ribbon and form events.

var Contoso = Contoso || {};
Contoso.CRM = Contoso.CRM || {};
Contoso.CRM.Account = Contoso.CRM.Account || {};

Contoso.CRM.Account.Ribbon = {
    helloWorldCommand: function () {
        Contoso.CRM.Account.Common.helloWorldCommand();
    }
}

Ribbon command script (TypeScript)

This example uses a minimal implementation, where the actual logic is in a separate file that is used both by the ribbon and form events.

module Contoso.CRM.Account.Ribbon {
    export function helloWorldCommand() {
        Common.helloWorldCommand();
    }
}

Common code used by Ribbon and Form (JavaScript)

var Contoso = Contoso || {};
Contoso.CRM = Contoso.CRM || {};
Contoso.CRM.Account = Contoso.CRM.Account || {};

Contoso.CRM.Account.Common = {
    helloWorldCommand: function () {
        alert("Hello World!");
    }
}

Common code used by Ribbon and Form (TypeScript)

module Contoso.CRM.Account.Common {
    export function helloWorldCommand() {
        alert("Hello World!");
    }
}

Form script (JavaScript)

Adds a keyboard shortcut in the form, to the ribbon button.

Contoso.CRM.Account.Form = {
    _privateMembers: {
        initialize: function() {
            //...
        }
    },

    onLoad: function() {
        // Register shortcuts using http://www.webreference.com/programming/javascript-keyboard-shortcuts/2.html
        shortcut.add("Ctrl+H", Contoso.CRM.Account.Common.helloWorldCommand, { 'type': 'keydown', 'propagate': true, 'target': document });

        Contoso.CRM.Account.Form._privateMembers.initialize();
    }
}

Form script (TypeScript)

Adds a keyboard shortcut in the form, to the ribbon button.

module Contoso.CRM.Account.Form {
    
    // private members
    function initialize() {
        //...
    }

    // public/exported members
    export function onLoad() {
        // Register shortcuts using http://www.webreference.com/programming/javascript-keyboard-shortcuts/2.html
        shortcut.add("Ctrl+H", Contoso.CRM.Account.Common.helloWorldCommand, { 'type': 'keydown', 'propagate': true, 'target': document });

        initialize();
    }
}
Advertisements

Limitations when using OData in CRM to retrieve records

This post describes the limitations of the OData endpoint in CRM 2011, which should still be relevant for CRM 2013 and 2015. There is no reason to believe that the OData endpoint will be developed further as Microsoft is working on a brand new WebApi to replace both the current OData endpoint and the SOAP endpoint.

Please refer to this article for general information on the available options when Qerying CRM using OData: OData system query options using the OData endpoint

Re-blogged from http://blogs.msdn.com/b/crm/archive/2011/03/02/using-odata-retrieve-in-microsoft-dynamics-crm-2011.aspx:

With the release of Microsoft Dynamics CRM 2011, Microsoft have added a Windows Communication Foundation (WCF) data services (ODATA) endpoint. The endpoint facilitates CRUD operation on entities via scripts using Atom or Json format. This post mentions some of the considerations when using the endpoint, specifically around the use of retrieves.

First, the operations supported over this endpoint are limited to create, retrieve, update and delete. The REST philosophy does not support other operations and so we followed J. Microsoft did not implement others since the story around service operations was not fully developed in the current WCF data services offering.

The $format and $inlinecount operators are not supported. CRM’s OData endpoint only supports $filter, $select, $expand, $top, $skip, $orderby

Some of the restrictions when using the implemented operators are.

Operator Restrictions
$expand
  • Max expansion 6
$top
  • Page size is fixed to max 50 records
  • $top gives the total records returned across multiple pages
$skip
  • When using with distinct queries, we are limited to the total (skip + top) record size = 5000.
  • In CRM the distinct queries does not use paging cookie are and so we are limited by the CRM platform limitation to the 5000 record.
$select
  • One level of navigation property selection is allowed I.e.

…/AccountSet?$select=Name,PrimaryContactId,account_primary_contact

…/AccountSet?$select=Name,PrimaryContactId,account_primary_

contact/LastName&$expand=account_primary_contact

$filter
  • Conditions on only one group of attributes are allowed. By a group of attribute I am referring to a set of conditions joined by And/Or clause.
  • The attribute group may be on the root entity

…/TaskSet?$expand=Contact_Tasks&$filter=Subject eq ‘test’ and Subject ne null

  • (or) on the expanded entity.

…/TaskSet?$expand=Contact_Tasks&$filter=Contact_Tasks/FirstName eq ‘123‘

  • Arithmetic, datetime and math operators are not supported
  • Under string functions, Substringof, endswith and startswith are supported
$orderby
  • Order are only allowed on the root entity.
Navigation
  • Only one level of navigation is allowed in any direction of a relationship. The relationship could be 1:N, N:1, N:N

Working with the OrganizationServiceContext

When developing .Net code that is working with CRM data through the CRM Organization Service, we use the OrganizationServiceProxy class of the CRM SDK. Alternatively, we can use the OrganizationServiceContext class, which is generated by crmsvcutil.exe, along with early bound proxy classes for each entity in our CRM organization. The OrganizationServiceContext class builds on top of the OrganizationServiceProxy, and adds a lot of extra functionality like tracking changes, managing identities and relationships, and gives you access to the Microsoft Dynamics CRM LINQ provider.

See more information and examples here:

Use the OrganizationServiceContext class

In most typical cases I will use the OrganizationServiceContext class in order to use Linq to query data in CRM. Linq is by far my favorite method to query CRM for the following reasons:

  • Queries can be written type safe and with 100% intellisense support. I.e. no strings in your code to specify attribute names. Resharper is also a big help wriiting Linq to CRM.
  • Compact queries, either with SQL like syntax or Lambda expressions.
  • Prettier and more readable code than FetchXML and QueryExpressions.
  • It is possible to UnitTest your queries, by stubbing the data sets in the CrmServiceContext, using Microsoft Fakes
  • A query can return more than one entity type at the same time, by constructing an anonymously typed container as return object.

Linq to CRM also have some limitations, for which you should use FetchXML or QueryExpressions instead. Most of all, it is missing outer joins and the “in” operator. Other limitations you should be avare of are described here:

Use LINQ to construct a Query

Another useful aspect of the OrganizationServiceContext is when modifying data in CRM. This is done by attaching objects (entities) to the service context, and submitting the changes using the SaveChanges method. However, you need to be aware that CRM records are by default automatically added to the context when performing Linq queries, so saving changes might have unforeseen consequences.

Before you start creating and updating object through the service context, I suggest reading this excellent post by Scott Durow:

Do you understand MergeOptions?

Comparison of OrganizationServiceProxy and OrganizationServiceContext

OrganizationServiceProxy

OrganizationServiceContext

Bulk insert/update supported

No. All Update/Create/Excecute calls are handled as individual web service calls.

Yes. All changes in the service context are processed in a single web service call with SaveChanges. This also support relating objects to each other in the context, and have multiple inter-relationships created in the same service call.

When to use

  • When you need simple CRUD operations on a single or a few records.
  • When you are using Linq and don’t understand merge options, or the differences when modifying objects with the two different approaches
  • When you are on CRM 2011 rollup 11 or earlier.
  • When you need to modify or create many objects at the same time, you should use bulk update for improved performance.
  • When you already tracking objects you have retrieved through the service context, you can reduce the code by just saving changes to the retrieved objects

Creation of objects

OrganizationService


var account = new Account
{
    Name = "Test account",
    Address1_City = "Copenhagen"
};
var accountId = service.Create(account);

NB: The id is returned

OrganizationServiceContext


using (var ctx = new CrmServiceContext(service))
{
    var account = new Account
    {
        AccountId = Guid.NewGuid(),
        Name = "Test account",
        Address1_City = "Copenhagen"
    };
    ctx.AddObject(account);

    ctx.SaveChanges();
}

NB: The id is not returned. Id’s should be assigned manually in code before saving if the id should be used later in the code. Alternatively; use ctx.AddLink() to have references between created objects automatically created in SaveChanges, whithout having to know the Ids beforehand:


using (var ctx = new CrmServiceContext(service))
{
    var account = new Account
    {
        Name = "Test account",
        Address1_City = "Copenhagen"
    };
    ctx.AddObject(account);

    var contact = new Contact
    {
        FirstName = "John",
        LastName = "Doe"
    };
    ctx.AddObject(contact);

    ctx.AddLink(contact, new Relationship("contact_customer_accounts"), account);

    ctx.SaveChanges(); // Creates two objects, and sets a reference between them
}

Update of objects

OrganizationService


var accountUpdate = new Account
{
    AccountId = accountId,
    Name = "Test Account"
};

service.Update(accountUpdate);

OrganizationServiceContext


using (var ctx = new CrmServiceContext(service))
{
    var accountUpdate = new Account
    {
        AccountId = accountId,
        Name = "Test Account"
    };
    ctx.Attach(accountUpdate); // The object is now tracked, and will be evaluated by SaveChanges()

    ctx.UpdateObject(accountUpdate); // Equivalent of OrganizationService.Update()

    ctx.SaveChanges(); // This performs the actual call against CRM 
}

As mentioned earlier, objects that are retrieved from CRM through the service context, are automatically tracked, based on the merge options in effect. This can lead to unexpected errors if you are not aware of the Automatic tracking of objects:

using (var ctx = new CrmServiceContext(service))
{
    var trackedAccount = ctx.AccountSet.First(a => a.AccountId.Value == accountId); // Automatically tracked in context

    var accountUpdate = new Account
    {
        AccountId = accountId,
        Name = "Test Account"
    };
    ctx.Attach(accountUpdate); // Error: System.InvalidOperationException: The context is already tracking a different 'account' entity with the same identity.
    ctx.UpdateObject(accountUpdate);           

    ctx.SaveChanges();
}

InvalidCastException when using MemoryCache in Custom CRM Workflow Activity

I have been struggling with the following error all morning.

I have a custom workflow activity in CRM 2011, that generates a PDF summary from CRM records. To maintain a reasonable good performance and flexibility, I have based the layout on a XML web ressource, where the layout configurations and images are cached using MemoryCache.

The code runs perfectly in Unit Test, but when running the actual workflow, I get the following error:

Workflow paused due to error: Unhandled Exception: System.InvalidCastException: [A]System.Collections.Generic.List`1[Crm.Pdf.PdfGenerator+AttributeDescription] cannot be cast to [B]System.Collections.Generic.List`1[Crm.Pdf.PdfGenerator+AttributeDescription]. Type A originates from 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' in the context 'LoadNeither' at location 'C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'. Type B originates from 'mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089' in the context 'LoadNeither' at location 'C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll'.
...
...
at System.Activities.CodeActivity.InternalExecute(ActivityInstance instance, ActivityExecutor executor, BookmarkManager bookmarkManager)
at System.Activities.Runtime.ActivityExecutor.ExecuteActivityWorkItem.ExecuteBody(ActivityExecutor executor, BookmarkManager bookmarkManager, Location resultLocation)

Explanation

It turned out the error occurs when caching types that are defined in the workflow assembly. CRM workflows are versioned, in the sence that running workflows will not update their implementation even if the actual workflow assembly is updated through plugin registration Tool. In this way, each time a workflow is writing to the MemoryCache, a new instance of the workflow assembly is being used. This causes a problem when fetching data from MemoryCache, because in order to cast the data back to the original type, not only must it be the same type and assembly, but also the same instance of the assembly.

Resolution

In order to make caching work in this scenario, there are two options:

  • Rewrite your caching code to store general types, for example by doing serialization
  • Add the custom types that needs caching to a separate assembly that can be stored in the Global Assembly Cache

I found the first option most preferable in my specific case, from a maintenance and deployment perspective.