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.

 

Advertisements

Using RequireJS in CRM2013

Javascript development often takes the backseat, compared to C#, while extending Dynamics CRM. There are valid reasons for implementing a business logic as a plugin or a custom workflow step. But, in this post I will talk only about how to effectively manage and write Javascript code for Dynamics CRM. These are my primary objectives:1.) Manage dependencies
2.) Control load sequence based on dependencies
3.) Promote code reuse
4.) Organise code effectively into manageable chunks

Dynamics CRM, doesn’t offer anything out-of-the box for accomplishing any of the above objectives. Ecmascript 6 will offer the much needed module feature1, but until all major browsers support Ecmascript 6, we can use any of the open-source script loaders. My framework of choice is require.js.

Current Process

The current process for Dynamics CRM script development:
1.) Create JS web resources
2.) Add web resources to the form
3.) Add event handlers for form load, form save and field change

Javascript webresources may not load in the order specified in the form2. This could cause script errors if you are expecting the scripts to load in a certain order. It is also cumbersome to add all the required scripts and setting up the event handlers.

Using require.js with Dynamics CRM 2013

RequireJS3 automatically loads dependent scripts/modules, when they are needed. Dynamics CRM, caching strategy4 means that we also need to know the magic string in the web resource url, so that we can use it in require.js. There are two ways to get this magic string:
1.) Parse it in the url of the initial loading script
2.) Use the undocumented WEB_RESOURCE_ORG_VERSION_NUMBER4 global

Setup

I have the following scripts
1.) Require.js – Require JS library
2.) Main.js – Initial loading script
3.) Form_script.js – Form script, contains event handlers for form load, form save and field change
4.) Common_script.js – Script library to be reused throughout the CRM organisation

Here is how the solution webresources are organised.

/// <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: {
            'lodash': {
                exports: '_'
            }
        },
        deps: ['lodash', 'common_script', 'form_script'],
        callback: function() {
            console.log('callback before requirejs has been loaded');
        },
        paths: {
            lodash: '../library/lodash'
        },
        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);
    }
})();

 

/// <reference path="XrmPage-vsdoc.js" />
/// <reference path="require.js" />
define(['common_script', 'lodash'], function (common,_) {
    console.log('loading form_script.js');
    //can use lodash, as it is specified as a dependency and should have been loaded
    console.log(_.VERSION);

    var form = {
        onSave: function(context) {
            common.log('onSave');
            common.log(context);
        },
        onLoad: function() {
            common.log('onLoad');
            Xrm.Page.data.entity.addOnSave(this.onSave);
            Xrm.Page.getAttribute('emailaddress1').addOnChange(function(context) {
                Xrm.Page.ui.setFormNotification('Email Address has changed', 'INFO', '1');
            });
        }
    };
    //call onload and start onload processing
    form.onLoad();
    return form;
});

 

/// <reference path="require.js" />
define(['lodash'], function (_) {
    console.log('loading common_script.js');
    //can use lodash, as it is specified as a dependency and should have been loaded
    console.log(_.VERSION);
    return {
        log: function(message) {
            if (console && console.log) {
                console.log(message);
            } else {
                alert(message);
            }
        }
    };
});

We first have to add the first two scripts, inside Dynamics CRM.

Here is the console output during script load.

Now we will verify whether the field change event handler fires on change of email field.

Now we will verify if the form save event is firing.

We have now successfully used requirejs to load scripts in Dynamics CRM.

EDIT (23/01/15): Have a look at https://dreamingincrm.com/2015/01/14/an-alternative-approach-to-loading-form-scripts-in-dynamics-crm/ for supported option using IFrame.

References
1. http://www.2ality.com/2013/07/es6-modules.html
2. http://wiki.ecmascript.org/doku.php?id=harmony:modules
3. http://www.develop1.net/public/post/Asynchronous-loading-of-JavaScript-in-CRM-2013.aspx
4. http://www.develop1.net/public/post/CRM-Developer-e28098Must-Knowe28099-2-Web-Resource-Caching.aspx

Unit Testing Plugins using Microsoft Fakes

Plugin Registration tool’s profiling functionality has made plugin debugging a lot easier. The ease of use can sometimes tempt you to use this feature in the early stages of plugin development, to analyse what is state of PluginExecutionContext, InputParameter, Pre/Post Images etc. This gives you an understanding of the plugin’s flow and state, but only after it has been deployed to CRM. This, however, is not unit testing.

In this post, I will explain how easy it is unit test a plugin using Microsoft Fakes. In this post, I am using CRM 2013 Developer Toolkit for plugin development. If you are new to plugin development and would like to know how to use the developer toolkit to develop plugins refer to Ben Hosk’s blog post http://crmbusiness.wordpress.com/2014/04/07/crm-2013-step-by-step-update-plugin-tutorial-using-the-crm-2013-development-toolkit/

What is Microsoft Fakes?

It is a unit testing framework, that can be used to do isolation testing using Shims and Stubs.

How can I get it?

Microsoft Fakes comes free with Visual Studio Premium and Visual Studio Ultimate

What is the process?

  1. Create your plugin project and associated plugin boilerplate code (generated by developer toolkit)
  2. Create a unit test project
  3. Build your plugin project
  4. Add the following references to your unit test project
    1. Your plugin assembly
    2. Microsoft.Xrm.Sdk
    3. System
    4. System.Core
    5. System.Runtime.Serialization
  5. Right click on Microsoft.Xrm.Sdk in your unit test project and click on Add Fakes Assembly
  6. Right click on System in your unit test project and click on Add Fakes Assembly
After you have added all your references you project should look something like this

Okay. Now what?

Now you can start writing your tests. To write an effective unit test you should start stubbing some classes in the Microsoft.Xrm.Sdk and overriding their behaviours. You have to use the following stubs inplace of their originals:
  1. StubIPluginExecutionContext
  2. StubITracingService
  3. StubIOrganizationService
  4. StubIServiceProvider

Scenario

You are developing a plugin which prevents the user from creating more than one opportunity for a customer.

Plugin Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace UnderstandingFakes
{
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Query;

    /// <summary>
    /// PreRegistrationPlugin Plugin.
    /// Fires when the following attributes are updated:
    /// All Attributes
    /// </summary>
    public class PreOpportunityPlugin : Plugin
    {
        /// <summary>
        /// Alias of the image registered for the snapshot of the
        /// primary entity's attributes before the core platform operation executes.
        /// </summary>
        private readonly string preImageAlias = "PreImage";

        /// <summary>
        /// Initializes a new instance of the <see cref="PreOpportunityPlugin"/> class.
        /// </summary>
        public PreOpportunityPlugin()
            : base(typeof(PreOpportunityPlugin))
        {
            base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(20, "Create", "opportunity", this.ExecutePreCreateAndUpdate));
            base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(20, "Update", "opportunity", this.ExecutePreCreateAndUpdate));

            // Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
            // You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
        }

        /// <summary>
        /// Executes the plug-in.
        /// </summary>
        /// <param name="localContext">The <see cref="LocalPluginContext"/> which contains the
        /// <see cref="IPluginExecutionContext"/>,
        /// <see cref="IOrganizationService"/>
        /// and <see cref="ITracingService"/>
        /// </param>
        /// <remarks>
        /// For improved performance, Microsoft Dynamics CRM caches plug-in instances.
        /// The plug-in's Execute method should be written to be stateless as the constructor
        /// is not called for every invocation of the plug-in. Also, multiple system threads
        /// could execute the plug-in at the same time. All per invocation state information
        /// is stored in the context. This means that you should not use global variables in plug-ins.
        /// </remarks>
        protected void ExecutePreCreateAndUpdate(LocalPluginContext localContext)
        {
            if (localContext == null)
            {
                throw new ArgumentNullException("localContext");
            }
            var context = localContext.PluginExecutionContext;
            Entity preImageEntity = (context.PreEntityImages != null && context.PreEntityImages.Contains(this.preImageAlias)) ? context.PreEntityImages[this.preImageAlias] : null;

            if (!context.InputParameters.Contains("Target"))
            {
                return;
            }
            var targetEntity = context.InputParameters["Target"] as Entity;
            var record = targetEntity;
            if (preImageEntity != null)
            {
                record = preImageEntity;
                foreach (var attribute in targetEntity.Attributes)
                {
                    record[attribute.Key] = targetEntity[attribute.Key];
                }
            }
            if (!record.Contains("customerid"))
            {
                return;
            }
            var customerLookup = record["customerid"] as EntityReference;
            var fetchOpportunities = string.Format(@"
            <fetch version=""1.0"" outputformat=""xmlplatform"" mapping=""logical"" distinct=""false"" count=""50"">
             <entity name=""opportunity"">
              <attribute name=""opportunityid"" />
              <order attribute=""name"" descending=""false"" />
              <filter type=""and"">
               <condition attribute=""customerid"" operator=""eq"" value=""{0}"" />
              </filter>
             </entity>
            </fetch>", customerLookup.Id);
            var opportunitiesResult = localContext.OrganizationService.RetrieveMultiple(new FetchExpression(fetchOpportunities));
            if (opportunitiesResult.Entities.Count > 0)
            {
                throw new InvalidPluginExecutionException(string.Format("You cannot create more then one opportunity for this {0}", customerLookup.LogicalName == "account" ? "Account" : "Contact"));
            }
        }
    }
}

Test Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace PluginTest
{
    using System.Diagnostics;
    using System.Fakes;

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Fakes;
    using Microsoft.Xrm.Sdk.Query;

    using UnderstandingFakes;

    [TestClass]
    public class PreOpportunityPluginUnitTests
    {
        private Entity TestEntity { get; set; }

        private static StubIServiceProvider ServiceProvider { get; set; }
        private static StubIPluginExecutionContext PluginExecutionContext { get; set; }
        private static StubIOrganizationService OrganizationService { get; set; }

        [ClassInitialize]
        public static void ClassInit(TestContext textContext)
        {
            var context = new StubIPluginExecutionContext();
            var tracingService = new StubITracingService();
            var orgFactory = new StubIOrganizationServiceFactory();

            ServiceProvider = new StubIServiceProvider();
            OrganizationService = new StubIOrganizationService();
            PluginExecutionContext = context;

            //override GetService behaviour and return our stubs
            ServiceProvider.GetServiceType =
                (type) =>
                {
                    if (type == typeof(IPluginExecutionContext))
                    {
                        return context;
                    }
                    else if (type == typeof(IOrganizationServiceFactory))
                    {
                        return orgFactory;
                    }
                    else if (type == typeof(ITracingService))
                    {
                        return tracingService;
                    }
                    return null;
                };
            context.UserIdGet = () => Guid.Empty;
            //return our stub organizationservice
            orgFactory.CreateOrganizationServiceNullableOfGuid = (userId) => OrganizationService;

            //write trace logs to output. only works when debugging tests
            tracingService.TraceStringObjectArray = (message, args) => Debug.WriteLine(message, args);
        }

        [TestInitialize]
        public void TestInit()
        {
            //setup initial values for each test
            var inputParameters = new ParameterCollection();
            PluginExecutionContext.InputParametersGet = () => inputParameters;
            TestEntity = new Entity();
            inputParameters.Add(new KeyValuePair("Target", TestEntity));
        }

        [TestCleanup]
        public void TestCleanup()
        {
            TestEntity = null;
        }

        [TestMethod]
        [ExpectedException(typeof(InvalidPluginExecutionException), "Exception not thrown")]
        public void Must_Throw_Exception_When_More_Than_One_Opportunity()
        {
            PluginExecutionContext.StageGet = () => 20;
            PluginExecutionContext.MessageNameGet = () => "Create";
            PluginExecutionContext.PrimaryEntityNameGet = () => "opportunity";
            TestEntity["customerid"] = new EntityReference("contact", Guid.Empty);

            //setup retrievemultiple behavior to return three entities. we are ignoring the customerid
            //in the previous line
            OrganizationService.RetrieveMultipleQueryBase = (query) =>
            {
                var result = new EntityCollection();
                result.Entities.Add(new Entity());
                result.Entities.Add(new Entity());
                result.Entities.Add(new Entity());
                return result;
            };

            var plugin = new PreOpportunityPlugin();
            plugin.Execute(ServiceProvider);
        }

        [TestMethod]
        public void Must_Not_Throw_Exception_If_Only_No_Opportunities()
        {
            PluginExecutionContext.StageGet = () => 20;
            PluginExecutionContext.MessageNameGet = () => "Create";
            PluginExecutionContext.PrimaryEntityNameGet = () => "opportunity";
            TestEntity["customerid"] = new EntityReference("contact", Guid.Empty);

            //return no results
            OrganizationService.RetrieveMultipleQueryBase = (query) =>
            {
                var result = new EntityCollection();
                return result;
            };
            var plugin = new PreOpportunityPlugin();
            plugin.Execute(ServiceProvider);
        }

        [TestMethod]
        public void Must_Not_Throw_Exception_If_No_CustomerId()
        {
            PluginExecutionContext.StageGet = () => 20;
            PluginExecutionContext.MessageNameGet = () => "Create";
            PluginExecutionContext.PrimaryEntityNameGet = () => "opportunity";
            OrganizationService.RetrieveMultipleQueryBase = (query) =>
            {
                var result = new EntityCollection();
                result.Entities.Add(new Entity());
                return result;
            };
            var plugin = new PreOpportunityPlugin();
            plugin.Execute(ServiceProvider);
        }
    }
}

Where  can I find more information about Microsoft Fakes?

Refer to the content posted by Visual Studio ALM rangers in Codeplex https://vsartesttoolingguide.codeplex.com/releases/view/102290

This just barely scratches the surface of what is possible. Please explore the codeplex site to know more about how you can utilise Fakes in your CRM Development.

EDIT (01/12/16): Use FakeXrmEasy or XrmUnitTest in your unit tests. Both the frameworks can do integration testing as well.