Showing posts with label javascript. Show all posts
Showing posts with label javascript. Show all posts

May 24, 2017

Xrm.Page.context.getVersion is now a supported method, use it!

Recently the MSDN page regarding the Client-side context methods of Dynamics 2016/365 has been updated and thanks to the Dynamics team a new method is inside the documentation: Xrm.Page.context.getVersion.

Some developers are already using this method in their code, personally I didn't because I had not the necessity to differentiate between the 8.x endpoints.

The method returns a string containing the full version in the Major.Minor.Build.Revision format, an example is "8.2.1.185".

In the previous CRM versions if we want to get the version from the client we had two ways:
  • call the RetrieveVersionRequest message using the SOAP endpoint (example)
  • check if a specific CRM function exists (example)
I used often the second way because I always considered doing a server call (often synchronous) not a good choice for the script performance.

Why we want to know the current CRM version? Because some functions are available only from a specific version, like the one to execute a workflow: the Microsoft.Dynamics.CRM.ExecuteWorkflow introduced with the 8.2 release.

But for the Web API endpoint we need to provide only Major.Minor and not the full version, therefore a transformation is necessary.

The following code maybe will be considered exaggerated by someone but creating a robust piece of code will reduce the necessity to change the script in the future:
function getCurrentVersion(context) {
    if (context.getVersion == undefined) { return ""; }
    var versionArray = context.getVersion().split(".");
    if (versionArray.length < 2) { return ""; }
    return versionArray[0] + "." + versionArray[1]; 
}
The result from the "8.2.1.125" is "8.2" ready to be inserted inside your Web API url.

Few considerations:
  • If the version cannot be retrieved I decided to return an empty string, but you can return null, throw an exception, call another method, set a default value, etc etc...
  • the function requires the context, in this way you can use in the form scripts (like an onload event) or inside a WebResource
  • I decided to split the string to an array using the dot (.) I think this is the most robust way compared to doing a substring or expecting a second dot in the string

September 5, 2016

Dependent OptionSet Generator - Overview - Part 1

Some days ago I released a new Dynamics CRM tool called Dependent OptionSet Generator.
As the name suggests it helps to create and update dependent OptionSets, one of the possible approaches to filter data inside CRM UI.

Scott Durow wrote an overview about this argument on his blog: "Option-Set, Lookup or Autocomplete", it's very useful if you plan to use dependent OptionSets in order to know the pro and cons of this approach.

I want to thank Scott not only for his post but because he found the time to meet me when I was in London a couple of months ago (photo). He was the first person I talked about this little project and we had a pleasant discussion not only about Dynamics CRM, thanks again Scott!
During my short visit in London I also met again Ramón Tébar Bueno and it's really nice to spend some time with other members of the Dynamics CRM Community, plus he took me to a typical British Pub!

Now back to the main reason of this post, the Dependent OptionSet Generator.
Starting from CRM 2011 the possibility to filter OptionSets entries was available thanks to the Xrm supported methods. Microsoft also provided a solution inside their CRM SDK to implement the dependent OptionSets but the most tedious process, the generation of the XML file required to map the entries, was delegated completely to the user.

Creating manually the XML document has always been a bummer, especially when you need to deal with several entries, so developers and consultants suggest to use filtered Lookups instead.
However there are implementations where OptionSets are preferred due to the Multi-Language support, to facilitate CRM users (at eCraft, the company I work for, we often have customers with Finnish and Swedish users) or because it's required by the law (for example in some provinces of Canada).

Microsoft kept updated its Dependent OptionSet solution during the years and with the latest CRM 2016 version they changed the configuration format structure from XML to JSON, probably in order to simplify the creation of the file.

My solution consists of two parts:
  • The first part is the JavaScript Library to be included inside the CRM forms in order to filter the OptionSets
  • The second part is a Web Application to build and maintain the JSON configuration required by the JavaScript Library
This post will cover the first part, I will write another post (or more) regarding the Web Application.

The JavaScript Library included in my solution is based on the CRM 2016 SDK version called sample_SDK.DependentOptionSetSample.js, you can find the source code here. Except for the first point (Different Namespace) all the other changes were done in order to make the script compatible with CRM 2011 supported browsers.

  • Different Namespace: because I didn't change the name of the functions I preferred to use a different namespace (DO instead of SDK) in order to avoid issues if the SDK library and my library are loaded in the same form.

  • JSON: The CRM 2016 library uses JSON, but the original Microsoft Library created for CRM 2011 uses XML. When the CRM 2011 library was created Dynamics CRM supported only Internet Explorer, including old versions like IE7. These old IE versions don't have the JSON object, so (probably) they decided to parse an XML object instead. My library in order to be compatible with these IE versions includes the JSON2 library. I can include it without issues because the first line of the library checks if JSON is already defined
    if("object"!==typeof JSON)JSON={};
    
    in this way old browsers can support JSON methods like JSON.parse.

  • getClientUrl: This Xrm method was introduced with CRM 2011 UR12, but my library in order to be compatible with pre-UR12 CRM 2011 instances checks if getClientUrl is available before using it. I used this code snippet in many of my samples in order to avoid creating a JavaScript only to handle CRM 2011 versions:
    var serverUrl;
    if (Xrm.Page.context.getClientUrl !== undefined) {
        serverUrl = Xrm.Page.context.getClientUrl();
    } else {
        serverUrl = Xrm.Page.context.getServerUrl();
        if (serverUrl.match(/\/$/)) { serverUrl = serverUrl.substring(0, serverUrl.length - 1); }
    }
    
  • response and responseText: Older versions of Internet Explorer don't implement the response property inside the object returned by the XmlHttpRequest, so if the property is not defined we will use the responseText instead.
    if (this.response !== undefined) { wrContent = this.response; }
    // handling responseText for older IE versions
    else { if (this.responseText !== undefined) { wrContent = this.responseText; } }
    
  • setTimeout: The setTimeout syntax used inside the SDK sample is not compatible with older IE versions, an easy fix is to wrap the function called inside an anonymous function:
    // original SDK sample code
    setTimeout(SDK.DependentOptionSet.filterOptions, 100, parentFieldParam, childFieldParam, dependentOptionSet);    
    // setTimeout rewritten for older IE versions
    setTimeout(function() { DO.DependentOptionSet.filterOptions(parentFieldParam, childFieldParam, dependentOptionSet); }, 100);
    
  • getClient and CRM for Tables and Phones: The SDK sample handles also the compatibility with CRM for mobile devices, but in order to handle the call to the method Xrm.Page.context.client.getClient() it's necessary to check if the context object contains the client property and its getClient method:
    if (Xrm.Page.context.client !== undefined && Xrm.Page.context.client.getClient !== undefined) {
        // ... rest of the code
    }
    
  • forEach: The SDK sample uses forEach to iterate the available values for the child OptionSet, but forEach is not available for older IE versions, the code has been rewritten to use a standard for cycle:
    //original SDK sample code
    validOptionValues.forEach(function (optionValue) { /* ... */ })
    // standard for cycle
    for (var count = 0; count < validOptionValues.length; count++) { /* ... */ }
    
Making the library compatible with CRM 2011 was more an exercise for me, CRM 2011 is officially not supported and I hope that many instances have been already migrated to newer versions, but I am aware that this is not always possible.

However the main part of my solution is the Web Application to create/update the JSON configuration, if you are already using the CRM 2016 SDK Sample in your CRM, you can use the Web Application without using my library and just update the Web Resources.

September 3, 2016

Passing Arrays to JavaScript Web Resources as parameter

Despite the availability of Business Rules, JavaScript is still an important way to customize Dynamics CRM UI, especially in complex scenarios (or when you just need to hide a section).

If you ever need to pass an Array as parameter inside the Event Handler form, you can declare it with the square brackets or using the Array keyword:
// square brackets
[1, 2, 3, 4 ,5]
// Array keyword
new Array(1, 2, 3, 4, 5)
CRM Event Handler:


As example we write a function to hide fields passing their logicalname as an array:
["name", "accountnumber", "telephone1"]
the function handles the parameter in this way:
function HideFields(fields) {
    for (var count = 0; count < fields.length; count++) {
        if (Xrm.Page.getControl(fields[count]) != null) {
            Xrm.Page.getControl(fields[count]).setVisible(false);
        }
    }
}
If a parameter is an Array, multiple parameters can still be passed inside the Event Handler. For example:
// note the comma before the array is declared
true, ["name", "accountnumber", "telephone1"]
In this way we are sending a boolean as first parameter to our updated function:
function ShowHideFields(isVisible, fields) {
    for (var count = 0; count < fields.length; count++) {
        if (Xrm.Page.getControl(fields[count]) != null) {
            Xrm.Page.getControl(fields[count]).setVisible(isVisible);
        }
    }
}
There is another way to pass multiple parameters without declaring each parameter inside the function definition and without using an Array: the arguments variable.
In JavaScript the arguments variable holds automatically all the parameters passed to a function.
We can rewrite the previous example, inside the Event Handler we pass the values as separate parameters, not as an array:
// no square brackets, no Array Keywords, just 3 separate parameters
"name", "accountnumber", "telephone1"
and our function is simply this one:
function HideFields() {
    for (var count = 0; count < arguments.length; count++) {
        if (Xrm.Page.getControl(arguments[count]) != null) {
            Xrm.Page.getControl(arguments[count]).setVisible(false);
        }
    }
}
But if we want to handle a specific element (like the first element) we need to take care inside the code:
// we decided that the first parameter will contain the value for the isVisible variable
true, "name", "accountnumber", "telephone1"
function ShowHideFields() {
    // this function requires at least 2 parameters
    if (arguments.length > 1) {
        var isVisible = arguments[0];
        // we start the for cycle from 1 instead of 0
        for (var count = 1; count < arguments.length; count++) {
            if (Xrm.Page.getControl(arguments[count]) != null) {
                Xrm.Page.getControl(arguments[count]).setVisible(isVisible);
            }
        }
    }
}

February 12, 2016

CRM 2016 Web API and plural names

The new Web API available for CRM 2016 is based on OData V4 protocol and the endpoint is /api/data/v8.0/

The MSDN contains several examples of the new endpoint, for example if we want to retrieve the top 3 accounts:

/api/data/v8.0/accounts?$select=name&$top=3

Simple, right? if you want to retrieve from account entity you write accounts, if you want to retrieve from contact entity you write contacts, if you want to retrieve from opportunity entity you write opportunities. You write the plural.

What a poor choice to use the plural form instead of the singular one, and let me say something, the root of the problem is an English-centered vision.

I'm not an expert of REST and OData protocols, maybe they suggest to use the plural forms but when you let users dynamically add entries to the endpoint (i.e. creating an entity) the use of plural forms is a mess.

A user on Dynamics Community asked a question: "my custom entity name is xxx_settings and I can't query using the endpoint /api/data/v8.0/xxx_settingss, which is the right syntax?"

Well, in this case the answer is simple, who designed the API decided to apply the exceptions, so if the noun ends with s the plural is es, in the user's case the correct endpoint is /api/data/v8.0/xxx_settingses, but if you want to be sure about the name you can check the metadata at this url: /api/data/v8.0/ or download the XML from the Developer Resources inside CRM UI.

Now let's play a game, how much are robust the exceptions they implemented in order to create the plural form? The answer is weak.
English is not my first language so I googled "plural exceptions english" and I landed in this page: A handout on irregular plural noun forms. I started to create some custom entities, here some results:
new_city become new_cities, English correct.
new_wolf become new_wolfs, English incorrect.
new_knife become new_knifes, English incorrect.
new_man become new_mans, English incorrect.

Now, I don't expect that all the irregular forms are covered, but if you decide to handle the final 's' and the final 'y' why not handle the final 'f'? However not big deal, You check the metadata and you can find the exact plural word to use.

BUT, and there is a BUT

Let's say we created our custom entity new_city and now the REST endpoint responds to /api/data/v8.0/new_cities, what will happen if we create a custom entity called new_citie?
The entity is not listed inside the REST endpoint!!! because the plural of new_citie is new_cities but it's already used by new_city.
The lesson is: pay attention when you name your entities if you need to query them using the new REST endpoint, if the plural form of a new entity is the same of the plural form of a previous entity, you can't query it.

July 14, 2015

New Dynamics CRM for phones iOS App and getFormFactor method

Yesterday Microsoft released the new iOS App for Dynamics CRM (iTunes link). The app only works with CRM versions 7.1 and higher, this means currently can be used only with CRM Online instances that received the Update 1.

The new App supports JavaScript so I wanted to test a little script with the method getFormFactor.

getFormFactor (MSDN link) is a new method introduced with CRM Online 2015 Update 1 returning a numeric value based on the current device. The possible values are:

ValueForm Factor
0Unknown
1Desktop
2Tablet
3Phone

The script I wrote is:
function accountOnLoad() {
    var factor = Xrm.Page.context.client.getFormFactor();
    var message = "The Form Factor is: " + factor;
    Xrm.Utility.alertDialog(message);
}
And the result is:

Note: the script is not called when a new record is created.

June 18, 2015

Hide fields dynamically based on Field Level Security privileges

When we set up Field Level Security for a field we can assign three different privileges (Read, Update, Write) to the users.
It is also possible to check the privilege using a supported JavaScript method: getUserPrivilege
This method returns an object with three boolean properties: canRead, canUpdate, canCreate.

We can use these properties in combination with the forEach method of attributes and controls collections in order to dynamically hide the fields.

The following code will hide dynamically all the fields protected by Field Level Security that the user can't read:
function HideSecuredFields() {
    // fetch all attributes on the form
    Xrm.Page.data.entity.attributes.forEach(
        function(attribute, aIndex) {
            // get the FLS privileges
            var privileges = attribute.getUserPrivilege();
            // check if the user can't read the field
            if (privileges.canRead == false) {
                // fetch all the controls related to the attribute
                attribute.controls.forEach(
                    function (control, cIndex) {
                       // hide the controls
                       control.setVisible(false);
                    }
                );
            }
        }
    );
}

March 30, 2015

JavaScript OData Pagination with synchronous calls

One limit of the OData endpoint is that the response can only include up to 50 records, so if your result set has more records it's necessary to reiterate the request using the url inside the __next property contained inside the returned object.

The following code shows how to fetch the request using synchronous calls, because sometimes you need (or want) to block the user :)
function getODataRecords(ODataUrl) {

    // we return an object with a similar structure as the OData endpoint
    var allRecords = new Object();
    allRecords.results = new Array();
    
    // we loop until we have an url to query
    var queryUrl = ODataUrl;
    while(queryUrl != null) {

        // we build the request
        var ODataRequest = new XMLHttpRequest(); 
        ODataRequest.open("GET", queryUrl, false); // false = synchronous request
        ODataRequest.setRequestHeader("Accept", "application/json"); 
        ODataRequest.setRequestHeader("Content-Type", "application/json; charset=utf-8"); 
        ODataRequest.send();

        if (ODataRequest.status === 200) {
            var parsedResults = JSON.parse(ODataRequest.responseText).d;
            if (parsedResults != null && parsedResults.results != null) {

                // we add the results to our object
                for (var i = 0; i < parsedResults.results.length; i++) {
                    allRecords.results.push(parsedResults.results[i]);
                }

                // check if there are more records and set the new url, otherwise we set to null the url
                if (parsedResults.__next != null) {
                    queryUrl = parsedResults.__next;
                } else {
                    queryUrl = null;
                }
            }
        } else {
            // if the request has errors we stop and return a null result
            queryUrl = null;
            allRecords = null;
        }
    }

    return allRecords;
}

// sample function to return all the accounts
function GetAllAccounts() {
    var serverUrl;
    if (Xrm.Page.context.getClientUrl !== undefined) {
        serverUrl = Xrm.Page.context.getClientUrl();
    } else {
        serverUrl = Xrm.Page.context.getServerUrl();
    }
    var ODataPath = serverUrl + "/XRMServices/2011/OrganizationData.svc";
    var accountQueryUrl = ODataPath + "/AcccountSet?$select=AccountNumber,Name";

    // call our new method
    var retrievedAccounts = getODataRecords(accountQueryUrl);

    // alert each result
    if (retrievedAccounts != null) {
        for (var i = 0; i < retrievedAccounts.results.length; i++) {
            alert(retrievedAccounts.results[i].Name + " - " + retrievedAccounts.results[i].AccountNumber);
        }
    }
}

March 3, 2015

Moment.js and Dynamics CRM: Multi Language support

The current version of Dynamics CRM supports 45 languages (an excel file with the list can be downloaded here: http://1drv.ms/18Ih0Ux) mapped to LCID codes.
This LCID code is also the one returned by the following methods: Xrm.Page.context.getUserLcid and Xrm.Page.context.geOrgLcid (the language of the user and the base language of the organization).

Moment.js is a powerful library to manipulate dates in JavaScript and it has support for internationalization but accepts only a locale string ("en", "it", ...) and not the LCID value returned by the above methods. I created a small JavaScript library (crm_lang.js) that accepts the 45 LCID CRM codes in order to return their corresponding Moment.js locale strings. The library can be downloaded from Technet Gallery:

http://gallery.technet.microsoft.com/scriptcenter/Momentjs-and-Dynamics-CRM-8bd3a264

Moment.js is very useful when comes to manipulate dates (add or subtracts days) and more important for this sample to display the date in various formats:
var now = moment();
alert(now.format());                                // "2015-03-03T08:02:17-05:00" (ISO 8601)
alert(now.format("dddd, MMMM Do YYYY, h:mm:ss a")); // "Tuesday, March 3rd 2015, 3:25:50 pm"
As I wrote before it supports internationalization, but it requires the locale string:
var now = moment();
now.locale("es"); // set the language to Spanish
alert(now.format("LLLL")); // "martes, 3 de marzo de 2015 8:00"
with the library I created, it's easier to set the right locale:
var now = moment();
var userLcid = Xrm.Page.context.getUserLcid(); //suppose is 1043 (Dutch)
var userLocale = CRMLanguages.getMomentLocale(userLcid);
now.locale(userLocale); // or now.locale(CRMLanguages.getMomentLocale(Xrm.Page.context.getUserLcid()));
alert(now.format("LLL")); // "3 maart 2015 08:00"
Moment.js constructor accepts a standard JavaScript date object, this makes easier to works with CRM form values:
var createdOn = Xrm.Page.getAttribute("createdon").getValue(); // get the date value
var m_date = moment(createdOn); // create the Moment.js object
m_date.locale(CRMLanguages.getMomentLocale(Xrm.Page.context.getUserLcid())); // set the language
m_date.add(3, "days"); // we add 3 days to the createdon date
alert(m_date.format("LLL")); // "2015年3月6日午前8時0分" //1041 Japanese
It's also possible to return the date as a standard JS object, the method is toDate()
// following the previous code
var threeDaysLater = m_date.toDate();
Xrm.Page.getAttribute("new_datecheck").setValue(threeDaysLater);
As you can see Moment.js can be very useful, make sure you read the documentation http://momentjs.com/docs/ and if you need a multi language support you can use my library. LLAP!

February 9, 2015

JSON and CRM Sandbox Plugins

Today I was working on a plugin and one of the requirements was a call to a web service passing some POST parameters, nothing complicated but I want to share part of the process.

Normally I register my plugins always inside Sandbox, the main reason is that I don't need to care where the plugin is executed (in this specific case the development is OnPremise but the production is Online), the second reason is that my user is often forgotten to be added as Deployment Administrator :)

One of the parameter was a JSON Object passed as string, practically I needed to do the C# equivalent of a JSON.stringify in order to pass a complex structure. An example can be the following Course object (JavaScript):
var Course = new Object();
Course.Name = "CRM Development 1";
Course.Teacher = "Prof. John Smith";

Course.Students = new Array();
Course.Students[0] = new Object();
Course.Students[0].ID = "001";
Course.Students[0].Name = "Walter Davis";

Course.Students[1] = new Object();
Course.Students[1].ID = "002";
Course.Students[1].Name = "Mark Harris";

var parameter1 = JSON.stringify(Course);
Because I was inside a Sandbox plugin I couldn't use the Newtonsoft.Json library and I didn't want to waste time trying to merge it inside my plugin.

The .NET framework provides different methods to create a JSON output, in particular JavaScriptSerializer (from System.Web.Script.Serialization namespace) and DataContractJsonSerializer (from System.Runtime.Serialization.Json namespace).

JavaScriptSerializer is very easy to use (it has a Serialize method returning a string) but doesn't work inside Sandbox, so I used DataContractJsonSerializer:
Course course = new Course();
// ...

string parameter1 = "";
using (MemoryStream memoryStream = new MemoryStream())
{
   DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(Course));
   serializer.WriteObject(memoryStream, course);
   parameter1 = Encoding.Default.GetString(memoryStream.ToArray());
}
A bit longer code but compatible with Sandbox and CRM Online.

October 9, 2014

The Xrm.Page.data.refresh method

CRM 2013 introduced new client APIs, one is the Xrm.Page.data.refresh method.
The MSDN definition is:
Asynchronously refreshes and optionally saves all the data of the form without reloading the page.
Xrm.Page.data.refresh(save).then(successCallback, errorCallback);
Why we leverage on this method?
The obvious reason is that we want the actual data displayed on the form, the inner reason is due to the Dynamics CRM platform, a record can be updated in several ways including workflows or plugins operating server-side.
Most of the server-side events don't refresh the UI, but with the Xrm.Page.data.refresh method we can manually force the page to display the current values.

If we want a simple refresh we can just write:
Xrm.Page.data.refresh(false);
A small advice will appear during the operation

August 16, 2014

Dynamics CRM Mug Shot

Note: This is just a demo done in my spare time, nothing less nothing more

Dynamics CRM 2013 has the ability to display an image inside the form but in my opinion too many clicks are required to change it. First you need to click the picture box, wait that the dialog is loaded, click browse..., find the picture in your pc, click Open and finally click OK.

Note: This is an unsupported customization

I created a JavaScript library to make easier this procedure, it enable the picture box to be a target for a drag&drop, so you just need to drag your picture and it will be saved to CRM!

Because the code will run in the OnLoad event, it will take a bit to modify the picture box, you will notice a dashed border when the picture box is enabled for drag&drop:

You can download the library from my OneDrive:
mugshot.js

It is necessary to add the library to the form and attach the InitializeMugShot function to the OnLoad event.

But wait there's more...

I created also a WPF Application (I'm not a big fan of WPF but has its advantages) to grab a snapshot from your camera with the size required by Dynamics CRM (144x144 pixels):



The snapshots listed in the right part are draggable, so you can drag the snapshot you just took from your webcam directly to the CRM picture box! (or you can drag them to the desktop if you want to save as file).

you can download the application from my OneDrive:
MugShot.zip

Enjoy!

August 7, 2014

Dynamics CRM special data types

Dynamics CRM includes some data types not available for custom fields. Because these field types act slightly different from the standard ones, it's necessary to pay attention during development.

This post is focused on the following data types:
  • Owner
  • Customer
  • Party List (ActivityParty)
Owner
Owner is a data type used for the built-in OnwerId field. This field is present only when an entity has the Ownership set to User or Team, it's not present if the entity's ownership is Organization. It can hold one user or one team record.

Customer
Customer data type is used when a field can hold one account or one contact record. Contact and Lead entities use this data type (ParentCustomerId and CustomerId respectively).

Party List
Party List (also known as Activity Party) is the most complex data type, it can hold more than one record at the same time and the possible entities are defined by the ParticipationTypeMask attribute.
For example the Email To field can hold all these entities:


Client-side (JavaScript)
Handling these data types in JavaScript is quite easy, they look like and act as standard lookup fields, we need to only pay attention to the entity LogicalName (property entityType) of the record(s).

Owner
// set the owner of the current record to a team
// note: this code works only when the form is on create method, to update the owner it's always necessary to use the AssignRequest message

var team = new Array();
team[0] = new Object();
team[0].id = "c9c77fd3-036a-47e0-857d-a4d56f21d8ee";
team[0].name = "Sales Team";
team[0].entityType = "team";
Xrm.Page.getAttribute("ownerid").setValue(team);

// check if the owner is a team or a user
var owner = Xrm.Page.getAttribute("ownerid").getValue();
if (owner != null) {
    alert("The owner type of this record is " + owner[0].entityType);
}
Customer
// set the parent customer of a contact

var parent = new Array();
parent[0] = new Object();
parent[0].id = "cd7f603d-e199-4b66-9ca7-3386093e9a50";
parent[0].name = "Test Account";
parent[0].entityType = "account";
Xrm.Page.getAttribute("parentcustomerid").setValue(parent);

// check if the customer of a lead is an account or a contact
var customer = Xrm.Page.getAttribute("customerid").getValue();
if (customer != null) {
    alert("The customer type of this lead is " + customer[0].entityType);
}
Party List
// set the to field of an email to a contact and a user

var recipients = new Array();
recipients[0] = new Object();
recipients[0].id = "72bf360d-3dd8-4538-a5ce-91a4be6af517";
recipients[0].name = "John Smith";
recipients[0].entityType = "contact";

recipients[1] = new Object();
recipients[1].id = "d4e1f6f5-1a2d-4571-bec7-9bd1ea1696d0";
recipients[1].name = "Tom Green";
recipients[1].entityType = "user";

Xrm.Page.getAttribute("to").setValue(recipients);

// get the total count of records inside the To field
var to = Xrm.Page.getAttribute("to").getValue();
if (to != null) {
    alert("The total count of records inside To field is " + to.length);
    alert("The last record type is " + to[to.length-1].entityType);
}
Server-side (C#)
Server-side we have some differences, although Owner and Customer data types are EntityReference, Party List type is an ActivityParty.

Owner
// set the owner of the current record to a team
// note: this code works only when the form is on create method, to update the owner it's always necessary to use the AssignRequest message

// late bound
EntityReference team = new EntityReference("team", new Guid("c9c77fd3-036a-47e0-857d-a4d56f21d8ee"));
entity["ownerid"] = team;

// early bound
EntityReference team = new EntityReference(Team.EntityLogicalName, new Guid("c9c77fd3-036a-47e0-857d-a4d56f21d8ee"));
MyEntity.OwnerId = team; 

// check if the owner is a team or a user

// late bound
if (entity.Contains("ownerid") && entity["ownerid"] != null) {
    EntityReference owner = (EntityReference)entity["owner"];
    Console.WriteLine("The owner type of this record is " + owner.LogicalName);
}
// early bound
if (MyEntity.OwnerId != null) {
    Console.WriteLine("The owner type of this record is " + MyEntity.OwnerId.LogicalName);
}
Customer
// set the parent customer of a contact

// late bound
EntityReference parent = new EntityReference("account", new Guid("cd7f603d-e199-4b66-9ca7-3386093e9a50"));
entity["parentcustomerid"] = parent;

// early bound
EntityReference parent = new EntityReference(Account.EntityLogicalName, new Guid("cd7f603d-e199-4b66-9ca7-3386093e9a50"));
MyEntity.ParentCustomerId = parent; 

// check if the customer of a lead is an account or a contact

// late bound
if (entity.Contains("customerid") && entity["customerid"] != null) {
    EntityReference customer = (EntityReference)entity["customerid"];
    Console.WriteLine("The customer type of this lead is " + customer.LogicalName);
}

// early bound
if (MyEntity.ParentCustomerId != null) {
    Console.WriteLine("The customer type of this lead is " + MyEntity.ParentCustomerId.LogicalName);
}
Party List
// set the to field of an email to a contact and a user

// late bound
Entity to1 = new Entity("activityparty");
to1["partyid"] = new EntityReference("contact", new Guid("72bf360d-3dd8-4538-a5ce-91a4be6af517"));
Entity to2 = new Entity("activityparty");
to2["partyid"] = new EntityReference("user", new Guid("d4e1f6f5-1a2d-4571-bec7-9bd1ea1696d0"));
entity["to"] = new Entity[] { to1, to2 };

// early bound
ActivityParty To1 = new ActivityParty();
To1.PartyId = new EntityReference(Contact.EntityLogicalName, new Guid("72bf360d-3dd8-4538-a5ce-91a4be6af517"));
ActivityParty To2 = new ActivityParty();
To2.PartyId = new EntityReference(SystemUser.EntityLogicalName, new Guid("72bf360d-3dd8-4538-a5ce-91a4be6af517"));
MyEntity.To = new ActivityParty[] { To1 , To2 }

// get the total count of records inside the To field

// late bound

if (entity.Contains("to") && entity["to"] != null) {
    EntityCollection to = (EntityCollection)entity["to"];
    Console.WriteLine("The total count of records inside To field is " + to.Entities.Count.ToString());
    if (to.Entities.Count > 0) {
        EntityReference lastparty = (EntityReference)to.Entities.Last()["partyid"];
        Console.WriteLine("The last record type is " + lastparty.LogicalName);
    }
}

//early bound
if (MyEntity.To != null) {
    Console.WriteLine("The total count of records inside To field is " + MyEntity.To.Count().ToString());
    if (MyEntity.To.Count() > 0) {
        Console.WriteLine("The last record type is " + MyEntity.To.Last().PartyId.LogicalName);
    }
}

July 25, 2014

Don't use getSelectedOption().text

OptionSet is a particular field in Dynamics CRM, basically is a key-value pair array, where the key is an integer and the value is a string.
Xrm Object provides several methods to interact with an OptionSet field, including the infamous getSelectedOption. But why a developer should avoid a supported Xrm method? The reason is simple, it can easily lead to JavaScript errors, as the getSelectedOption().text case.

getSelectedOption returns the selected option as an object with this structure:
// CustomerTypeCode OptionSet - Account Entity
{"value": 8, "text": "Prospect"}
It's true that we can access the object properties in order to get the label using the syntax getSelectedOption().text (and getSelectedOption().value) but only if the OptionSet field is not empty. If there isn't a value selected, the getSelectedOption() will return null and not an object.
// this code generates an error if the field is empty
var customerType = Xrm.Page.getAttribute("customertypecode").getSelectedOption().text;
A workaround is to check if the object is not null:
var customerType = "";
var option = Xrm.Page.getAttribute("customertypecode").getSelectedOption();
if (option != null) {
    customerType = option.text;
}
But Xrm object provides a separate method to retrieve the text of a selected OptionSet, this method is getText, and more important when no option is selected, getText will return an empty string value.

Error-free code:
var customerType = Xrm.Page.getAttribute("customertypecode").getText();
In the same way the standard getValue() must be used instead of getSelectedOption().value, it returns the integer selected value or null if the field is empty:
var customerValue = Xrm.Page.getAttribute("customertypecode").getValue();
if (customerValue != null) {
    // ...
} else {
    // ...
}
Note that getSelectedOption() can be still used in some scenarios, for example when we manipulate the OptionSet using AddOption,RemoveOption or getOptions methods and we want to compare the selected option as object with other values, but never use it to just retrieve the selected OptionSet label.

June 20, 2014

LCID JavaScript Helper Library

Xrm.Page object provides two methods to get the relevant language used inside Dynamics CRM:
  • Xrm.Page.context.getUserLcid() - returns the LCID value that represents the Microsoft Dynamics CRM Language Pack that is the user selected as their preferred language
  • Xrm.Page.context.getOrgLcid() - returns the LCID value that represents the base language for the organization
The value returned is the lcid decimal value, there is not a built-in function to return the culture name ("en-US" for LCID 1033) or the language region ("German (Germany)" for LCID 1031).

The attached library provides an easy way to get the culture name and the language region from the decimal lcid value, it contains two methods:

  • LCIDUtils.getCultureName(lcid)
    returns a string with the culture name of the selected lcid value
    var userLcid = Xrm.Page.context.getUserLcid(); 
    var userCultureName = LCIDUtils.getCultureName(userLcid); 
    alert("User Culture name is: " + userCultureName);
    
  • LCIDUtils.getLanguageRegion(lcid)
    returns a string with the language region of the selected lcid value
    var orgLcid = Xrm.Page.context.getOrgLcid(); 
    var orgLanguageRegion = LCIDUtils.getLanguageRegion(orgLcid); 
    alert("Organization Language Region is: " + orgLanguageRegion);
    
The library can be downloaded from Technet Gallery:
http://gallery.technet.microsoft.com/scriptcenter/LCID-JavaScript-Helper-7cfb0829

Note: The culture name and language region mappings come from this MSDN page: National Language Support (NLS) API Reference (Windows 7)

July 16, 2013

How to bind a function to the Sub-Grid refresh event

A common customization requested by customers is to execute some logic when a Sub-Grid is refreshed. Unfortunately the Xrm Object Model doesn't provide a method to bind a function to the refresh event.
To implement this functionality is necessary to use unsupported JavaScript code, paying attention when a new rollup is released.
The following code is compatible with UR12/Polaris update that introduced cross-browser support for CRM 2011.
function AddEventToGridRefresh(gridName, functionToCall) {
   // retrieve the subgrid
   var grid = document.getElementById(gridName);
   // if the subgrid still not available we try again after 1 second
   if (grid == null) {
       setTimeout(function () {AddEventToGridRefresh(gridName, functionToCall);}, 1000);
       return;
   }
   // add the function to the onRefresh event
   grid.control.add_onRefresh(functionToCall);
}
Is possible to use the AddEventToGridRefresh function in two different ways. Assuming to attach the following function:
// function used in this example
function AdviseUser() {
   alert("Sub-Grid refreshed");
}
The function can be called directly passing the parameters inside the dialog box


or inside another JavaScript function:
function OnLoad() {
   AddEventToGridRefresh('accountContactsGrid', AdviseUser);
}
The first parameter is a string and quotation marks are necessary, the second parameter is the name of the function and must be written without quotation marks.

July 9, 2013

Set Account's Primary Contact as Recipient when creating a new Phone Call

When a new Phone Call is created starting from an Account, the default recipient is the Account itself.
If we want to set the Primary Contact of the Account as Recipient, we need to call the following function inside the OnLoad event of the Phone Call entity.
function ChangePhoneCallRecipientFromAccountToPrimaryContact() {
    // check if is a new phone call
    if (Xrm.Page.ui.getFormType() == 1) {
        // get the Phone Call To Recipient
        var to = Xrm.Page.getAttribute("to").getValue();
        // if the Recipient is an account we continue
        if (to != null && to[0].entityType == "account") {
            // get the account Id
            var accountId = to[0].id;    
            // get the right url for the OData Query
            var serverUrl;
            if (Xrm.Page.context.getClientUrl !== undefined) {
                serverUrl = Xrm.Page.context.getClientUrl();
            } else {
                serverUrl = Xrm.Page.context.getServerUrl();
            }
            // build the request
            var ODataPath = serverUrl + "/XRMServices/2011/OrganizationData.svc"; 
            var accountRequest = new XMLHttpRequest();
            accountRequest.open("GET", ODataPath + "/AccountSet(guid'" + accountId + "')", false); 
            accountRequest.setRequestHeader("Accept", "application/json"); 
            accountRequest.setRequestHeader("Content-Type", "application/json; charset=utf-8");
            // execute the request
            accountRequest.send();
            if (accountRequest.status === 200) {
                var retrievedAccount = JSON.parse(accountRequest.responseText).d;
                // retrieve the primary contact of the account
                var primaryContact = retrievedAccount.PrimaryContactId;
                // if there is a primary contact we set as new To Recipient
                if (primaryContact.Id != null) {
                    var newTo = new Array();
                    newTo[0] = new Object();
                    newTo[0].id = primaryContact.Id;
                    newTo[0].name = primaryContact.Name;
                    newTo[0].entityType = primaryContact.LogicalName;
                    Xrm.Page.getAttribute("to").setValue(newTo);
                }
            }
            else {
                alert("error");
            }
        }
    }
}

June 28, 2013

OptionSet JavaScript Helper Library

Xrm.Page object provides several methods to work with OptionSet attributes, the most used are:
  • Xrm.Page.getAttribute("optionset").getValue() - returns the selected item value
  • Xrm.Page.getAttribute("optionset").getText() - returns the selected item label
Sometimes is necessary to access all the items of an Optionset and not only the selected one, for this purpose I wrote a JavaScript library.

The library uses only supported JavaScript and doesn't query the metadata to retrieve the labels. It contains the following functions:

  • opt.GetValues(fieldName)
    returns an array with all the values of a specific OptionSet
    var freightTermsValues = opt.getValues("address1_freighttermscode");
    alert("Values inside Freight Terms OptionSet: " + freightTermsValues);
    
  • opt.GetLabels(fieldName)
    returns an array with all the labels of a specific OptionSet
    var shippingMethods = opt.getLabels("address1_shippingmethodcode");
    alert("Available Shipping Methods: " + shippingMethods);
    
  • opt.GetLabel(fieldName, value)
    returns the corresponding label for a specific OptionSet value
    var paymentTermLabel = opt.getLabel("paymenttermscode", 2);
    alert("Payment Term with Value 2 is: " + paymentTermLabel);
    
  • opt.GetValue(fieldName, label)
    returns the corresponding value for a specific OptionSet label
    var primaryValue = opt.getValue("address1_addresstypecode", "Primary");
    alert("Value for address type Primary is: " + primaryValue);
    
Library code:
if (typeof (opt) == "undefined") { opt = {}; }

opt.getValues = function (fieldName) {
    var values = [];
    var attribute = Xrm.Page.getAttribute(fieldName);
    if (attribute != null && attribute.getAttributeType() == "optionset") {
        var options = attribute.getOptions();
        for (var i in options) {
            if (options[i].value != "null") values.push(options[i].value * 1);
        }
    }
    return values;
};

opt.getLabels = function (fieldName) {
    var labels = [];
    var attribute = Xrm.Page.getAttribute(fieldName);
    if (attribute != null && attribute.getAttributeType() == "optionset") {
        var options = attribute.getOptions();
        for (var i in options) {
            if (options[i].value != "null") labels.push(options[i].text);
        }
    }
    return labels;
};

opt.getLabel = function (fieldName, value) {
    var label = "";
    var attribute = Xrm.Page.getAttribute(fieldName);
    if (attribute != null && attribute.getAttributeType() == "optionset") {
        var option = attribute.getOption(value);
        if (option != null) label = option.text;
    }
    return label;
};

opt.getValue = function (fieldName, label) {
    if (label == "") return null;
    var value = null;
    var attribute = Xrm.Page.getAttribute(fieldName);
    if (attribute != null && attribute.getAttributeType() == "optionset") {
        var options = attribute.getOptions();
        for (var i in options) {
            if (options[i].text == label) return options[i].value * 1;
        }
    }
    return value;
};
Note: developed with Xrm JavaScript Dojo

April 30, 2013

April 16, 2013

April 8, 2013

Disable CRM 2011 Form Assistant for selected fields

the Form Assistant is a useful feature that allows to populate some lookup fields by providing a list on the right side of the form.



It's possible to disable completely the Form Assistant, but there is a way to disable the functionality only for some fields of the form?

To achieve this result we need to manipulate the DOM with JavaScript,