April 27, 2015

GetAttributeValue demystified

Note: the content of this post is similar to this post from Dave Berry but I wanted to approach the same argument with some example code and tips.

Dynamics CRM allows to create fields with the following data types:

Except for "Single Line of Text" and "Multiple Lines of Text" (both use string), each one uses a different underline data type in .NET, some of these data types are nullable, some are not.

What means nullable and why we need to care about this?
Let's start with an example: In our CRM we have two records, the first record has all the fields filled with a value, in the second one all the fields are empty. When we use GetAttributeValue and there is a value, the method (fairly) returns the value. But what happens with our empty record? The response is "depends".

GetAttributeValue uses Generics, so we can choose to get a nullable type or not:
bool boolean = entity.GetAttributeValue<bool>("new_boolean");
bool? booleanNullable = entity.GetAttributeValue<bool?>("new_boolean");
In this case, if the value is null (for a Boolean/Two Options field means that no value is set) the first variable will contains false, the second will contains null.
The next table is a summary:

CRM Type .NET Type can hold null? default value
Single Line of Text string Yes
Option Set OptionSetValue Yes
Two Options bool No false
Image byte[] Yes
Whole Number int No 0
Floating Point Number double No 0.0
Decimal Number decimal No 0
Currency Money Yes
Multiple Lines of Text string Yes
Date and Time DateTime No DateTime.MinValue
Lookup EntityReference Yes
For the types that can hold null, we use GetAttributeValue and after check if it's null or not:
EntityReference lookupRef = entity.GetAttributeValue<EntityReference>("new_lookupid");
if (lookupRef == null) {
   // no value set
} else {
   // we have a value
}
For the types that can't hold null we need to ask ourselves: "The default value is enough for the requirement?"

If we are in a loop and we need to do a sum of an int field, the default value (0) is ok, so we can just do
int totalSum = 0;
foreach (Entity row in RowCollection.Entities) {
   int number = entity.GetAttributeValue<int>("new_wholenumber");
   totalSum+=number;
}
but if we are doing a multiplication we need to skip the null values, so we use int?
int totalMulty = 0;
foreach (Entity row in RowCollection.Entities) {
   int? number = entity.GetAttributeValue<int?>("new_wholenumber");
   if (number != null) {
      totalMulty*=number;
   }
}
otherwise with a null value our totalMulty variable will be 0.

DateTime is a particular case, it can't hold null but the default value (MinValue = 01/01/0001) can't be a valid CRM value (as happens with bool and numeric fields) so we can do the following check:
DateTime dateTime = entity.GetAttributeValue<DateTime>("new_datetime");
if (dateTime == DateTime.MinValue) {
   // no value set
} else {
   // we have a value
}
Practically when we don't use the nullable form, Dynamics CRM is doing the following:
double floating = entity.GetAttributeValue<double>("new_floating");
// equals to
double floating = entity.GetAttributeValue<double?>("new_floating").GetValueOrDefault();
The combination of the nullable form and the GetValueOrDefault can be useful in some scenarios. Let's say that we need to do a data migration to an external system, but if the source decimal Quantity is null, the target decimal Quantity must be -1.
decimal quantity = entity.GetAttributeValue<decimal?>("new_quantity").GetValueOrDefault(-1);
In this way we deal automatically the null values and they are ready for the target system.

Now you are a Dynamics CRM True Survivor!

April 2, 2015

Simplified Connection with complicated passwords

CRM 2011 introduced a very easy way to connect to Dynamics CRM instances: the Simplified Connection (MSDN: https://msdn.microsoft.com/en-us/library/gg695810.aspx).
Basically it's necessary to build a connection string instead of dealing with the specific deployment type (OnPremise, IFD or Online).

The downside of using a Simplified Connection is its weakness management of passwords containing special characters like double quotes, single quotes, ampersands.

Considering the MSDN example for a CRM Online connection:
Url=https://contoso.crm.dynamics.com; Username=jsmith@contoso.com; Password=passcode;
If the password is ;abc123 (note the semicolon) an exception will be thrown with the following message:
Format of the initialization string does not conform to specification starting at index 102. The solution for this problem is to include the password inside single quotes, the following connection string will work:
Url=https://contoso.crm.dynamics.com; Username=jsmith@contoso.com; Password=';abc123';
Assuming the connection string is builded dynamically the following code can be used:
string connectionString =
    String.Format("Url={0}; Username={1}; Password='{2}';", url, username, password);
What if our complicated password contains single quotes as well? Let's consider for example the following password: ;a''bc'123
In this case the previous exception (Format of the initialization string) will be thrown again. This issue can be solved "escaping" the single quotes using a Replace:
string connectionString =
    String.Format("Url={0}; Username={1}; Password='{2}';",
    url, username, password.Replace("'","''"));
Please note that the escape must be done also if your connection string is stored inside your app/web.config:
<add key="CRM"
value="Url=https://contoso.crm.dynamics.com; Username=jsmith@contoso.com; Password=';a''bc'123';"/>
But in this case our replace method will not work, because it will replace also the single quotes delimiting the password. In this scenario I suggest to put inside the app/web.config a placeholder instead of the delimiting single quotes that will be replaced after (for example #XYZ#):
<add key="CRM"
value="Url=https://contoso.crm.dynamics.com; Username=jsmith@contoso.com; Password=#XYZ#;a''bc'123#XYZ#;"/>
Then after the connection string is loaded we do the escape and the replace:
string connectionString = ConfigurationManager.ConnectionStrings["CRM"].ConnectionString;
// escape the single quotes inside the password
connectionString = connectionString.Replace("'","''");
// replace the placeholder with single quotes
connectionString = connectionString.Replace("#XYZ#","'");
Of course this will not work if the password contains the placeholder as well, so it's better to choose a long placeholder.

When the password is stored inside the app/web.config it's necessary to deal with another problem, the case that our password contains XML special characters (mostly double quotes) because this file is an XML.

If it's necessary to encode the password the following .NET method can be used:
string xmlPassword = System.Security.SecurityElement.Escape(password);
The result for the password ;a''b"c'123 (note the double quote between b and c that will create problems if not encoded) will be ;a&apos;&apos;bc&apos;123, a valid string to be written inside the app/web.config.

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 9, 2015

Japan and Australia Data Center Discovery URLs

Microsoft launched the new Data Center in Japan for CRM Online, you can read the announcement by Bob Stutz here:
Japan Datacenter Open for Business: Now Serving Japanese CRM Online Customers.

The Australia Data Center is also operative and available for companies from Australia, New Zealand and Fiji.

Microsoft defined also the Discovery Web Service URL of these data centers, the addresses are:
https://disco.crm7.dynamics.com/XRMServices/2011/Discovery.svc (Japan)
https://disco.crm6.dynamics.com/XRMServices/2011/Discovery.svc (Australia)

The Japanese data center is identified by the crm7 host, the Australian one by crm6. This will affect also the organization url, for example if Contoso Ltd. has two branches, one in Japan and the other one in Australia, their urls will be:
https://contosojapan.crm7.dynamics.com
https://contosoaustralia.crm6.dynamics.com

The relative MSDN page has been updated to include this change, the next table is a recap:

HostLocation
crmNorth America
crm4EMEA
crm5APAC
crm2South America
crm9North America 2 (CRM Online for Government)
crm7Japan
crm6Australia

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.

January 27, 2015

New CRM 2015 exams available: MB2-707 Customization & Configuration and MB2-706 Online Deployment

The following CRM 2015 exams are now available:

MB2-706: Microsoft Dynamics CRM Online Deployment
MB2-707: Microsoft Dynamics CRM Customization and Configuration

The exams can be already scheduled and until 31 May 2015 there is also the Second Shot offer, meaning that you can retake the exam for free if you didn't pass the first time.

All the Dynamics CRM 2015 exams are listed here:
Dynamics CRM 2015 Certifications