Gotcha: Optional Action Parameters

Yesterday, when I was looking into some failed tests, I discovered a quirk of action. If you invoke an action and don’t pass in the arguments that are optional, it doesn’t mean this is going to come up as null in the execution context.

Below is the action I used to replicate the behaviour.

Optional

When I execute the action and don’t set any arguments, here is what comes through in the execution context.

Argument Type Value
 Boolean  false
 DateTime  0001-01-01T00:00:00
 Decimal  0
 Entity  null
 EntityReference  null
 Float  0
 Int  0
 Money  null
 Picklist  null
 String  null
 EntityCollection  null

The interesting things to note are:

  1. Action arguments, don’t behave the same as AttributeCollection. When you retrieve an entity using the SDK, get the attribute just using the key (not GetAttributeValue), the key won’t exist in the collection, if the attribute value is null. This is not the same behaviour with the action arguments. The key will always be there in the context, even if was not set during the action invocation.
  2. The default value of action arguments = default value of the primitive type

This essentially means that if you are going to do some processing based on the action argument that is a primitive type (Boolean, DateTime, Decimal, Float, Int, String), don’t make it optional. For e.g. if you make a boolean argument optional, how would you differentiate between an action invocation with the argument set to false, and another one which did not set the argument at all?

tl;dr; For action arguments (both input and output) of type Boolean, DateTime, Decimal, Float, Int and String avoid optional.

Highlighting specific records in a view

I recently had a requirement to highlight certain records that are displayed using a System View. It is not possible to use different colours in a supported way to accomplish this. It is possible to rollout your own custom grid using libraries like SparkleXrm and embed this on a form using IFrames. The only problem with this is you still can’t highlight a record in System View using SparkleXrm. While it is possible to create a new system view that contains just the records that need to be highlighted, this is not a true highlighting.

A quick solution is to use a simple text attribute, and store a unicode character. These are the miscellaneous symbols in Unicode. You could use the “★” symbol for highlighting the record.

Here is screenshot of what I have done.

All I have done is added a simple text attribute to the entity, and stored “★” character in records that need to be highlighted, and added this to the System View that needs this highlighting. A workflow or business rule could be used to set this attribute based on the highlighting logic. This approach can be used to quickly “highlight” certain records. If you have done this a different way, I would love to hear your approach.

Update Tooltip using XrmToolBox Easy Translator

XrmToolBox is one of my favorite tools, that I use everyday. Among the 20+ tools in XrmToolbox, these are my top three by  usage
1.) Sitemap Editor
2.) Web Resources Manager
3.) View Layout Replicator

Among all these tools, which are all exceptional, Easy Translator is one I never used, as I work only on CRM Deployments with English as the primary language and no requirement from any of the users for alternate languages. There is a interesting usecase for this tool, apart from translation. You can use this to update the tooltip text is CRM 2013. Tooltip text in CRM 2013 is picked up from the description on the field property.

The account name field will show the tooltip text as “Type the company name or business name” when the user hovers over the label.
If we want to setup meaningful tooltips for lot of fields, it is easier to do this via Easy Translator. First export the selected entity to Excel using Easy Translator. I this case I have just chosen Account. We just want to export only the Attributes.
Next open the file in Excel and update Description for the attributes that require a different tooltip text, I this case I am going to change the tooltip text only for the Account Name field to “Type the organisation name or business name“.
Save the Excel file and import it.
The tooltip text should now reflect your changes.

Executing QuickFind using CRM SDK

Global search in CRM for Tablets executes Quickfind view across the entities specified in the System Settings.

Internally the tablet client uses ExecuteQuickFindRequest to perform this quick find search. We can perform the same request using OrganizationRequest. Let’s look at the code.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Tooling.Connector;
using System.Net;

namespace ExecuteQuickFind
{
    class Program
    {
        static void Main(string[] args)
        {
            var crmConnection = new CrmConnectionHelper(new NetworkCredential("[username]","[password]","CRM"),AuthenticationType.AD, "crm1","80","Contoso");
            if(crmConnection.IsReady)
            {
                var executeQuickFindRequest = new OrganizationRequest("ExecuteQuickFind");
                executeQuickFindRequest.Parameters = new ParameterCollection();
                var entities = new List<string> { "contact", "lead","opportunity","systemuser","competitor","activitypointer", "incident" };
                //specify search term
                executeQuickFindRequest.Parameters.Add(new KeyValuePair<string object="">("SearchText","maria"));
                //will cause serialisation exception if we don't convert to array
                executeQuickFindRequest.Parameters.Add(new KeyValuePair<string object="">("EntityNames", entities.ToArray()));
                
                var executeQuickFindResponse = crmConnection.OrganizationServiceProxy.Execute(executeQuickFindRequest);
                var result = executeQuickFindResponse.Results.FirstOrDefault();
                if (executeQuickFindResponse.Results.Any())
                {
                    var quickFindResults = result.Value as QuickFindResultCollection;

                    if (quickFindResults != null)
                    {
                        foreach (var quickFindResult in quickFindResults)
                        {
                            if (quickFindResult.ErrorCode != 0)
                            {
                                Console.WriteLine("Quickfind for {0} errored with code {1}",
                                                  quickFindResult.Data.EntityName,
                                                  quickFindResult.ErrorCode);
                                continue;
                            }
                            Console.WriteLine("***Entity {0} returned {1} record(s)***", quickFindResult.Data.EntityName,
                                              quickFindResult.Data.Entities.Count);
                            foreach (var entityRow in quickFindResult.Data.Entities)
                            {
                                foreach (
                                    var attribute in
                                        entityRow.Attributes.Where(
                                            attribute => !entityRow.FormattedValues.Any(x => x.Key == attribute.Key)))
                                {
                                    Console.WriteLine("{0} = {1}", attribute.Key, ExtractValue(attribute.Value));
                                }
                                foreach (var formattedAttributes in entityRow.FormattedValues)
                                {
                                    Console.WriteLine("Formatted: {0} = {1}", formattedAttributes.Key,
                                                      formattedAttributes.Value);
                                }
                                Console.WriteLine("-----------------------------------------");
                            }
                        }
                    }
                }               
            }
        }

        private static object ExtractValue(object attributeValue)
        {
            var attributeType = attributeValue.GetType().Name;
            object returnValue = attributeValue;
            switch (attributeType)
            {
                case "OptionSetValue":
                    returnValue = ((OptionSetValue) attributeValue).Value;
                    break;
                case "EntityReference":
                    returnValue = ((EntityReference)attributeValue).Name;
                    break;
                case "Money":
                    returnValue = ((Money)attributeValue).Value;
                    break;
            }
            return returnValue;
        }
    }
}

Here is the result after executing this code.

The maximum number of entities you can specify in the EntityNames parameter for this ExecuteQuickFindRequest is 10.

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.

 

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.

Changing the default view of an inline lookup in CRM 2013

Dynamics CRM 2013 has done away with the ribbon interface, with the exception of a few forms. There are also some changes in the subgrid behavior. These are

1.) When you select a record in a subgrid ribbon menu no longer appears on top of the page
2.) Clicking the “+” icon above the subgrid, may display an inline lookup

I say “may” because it depends on the relationship. An inline lookup is only displayed when the parent entity field is NOT mandatory on the child record you are trying to create. When the inline lookup is displayed on a subgrid, there is no way to access this inline lookup control using CRM Client API.

There is however, an unsupported way to set the default view on an inline lookup. I am using Ribbon Workbench, by Scott Durow, as it makes the process much easier.

First notedown the name of the subgrid. In this example, I am trying to customise the contact subgrid on account. Note this down, as it is required in the later steps.


In this case, the subgrid is called accountContactsGrid. Next we have to get the GUID of the view we want to default to. There are two ways to do this: looking into the database or using Developer Tools. Open an account record, and try to add a contact to the subgrid. When the lookup selection popup appears, use Developer Tools to get the GUID of the required view. In this case, we want the GUID of Contacts: No Campaign Activities in Last 3 Months view. Note this down, as it also required in the script we will add later.



Open the solution with the contact entity in Ribbon Workbench. In this case I am trying to change the behavior of the contact subgrid. Ribbon Workbench displays the following subgrid menu for the contact entity.

Since this is a 1:M relationship and account relationship field is not mandatory on contact record, the first Add Existing button has to be customised. Right click on the first Add Existing button and click Customise Command.

The default command behavior is as below

The Mscrm.GridRibbonActions.addExistingFromSubGridStandard function is called with the entity typecode and the grid control as parameters. This has to be changed to call our custom function in a JavaScript webresource.

Create, upload and publish a new Javascript webresource with the code below. Since the contact entitity can be a subgrid in other entities as well, we want to restrict this behavior only to account entity. We can further lock down this behavior to a specific form on the account entity, if we want, but we are not going to do this in this instance.

function () {
    var NYR = window["NYR"] || {};
 
    NYR.addExistingContactSubgrid = function (gridTypeCode, gridControl) {
        Mscrm.GridRibbonActions.addExistingFromSubGridStandard(gridTypeCode, gridControl);
        if (Xrm.Page.data.entity.getEntityName() === 'account' &&
            gridTypeCode === 2 &&
            typeof gridControl['get_viewTitle'] === 'function') {
                var inlineBehaviour = document.getElementById('lookup_accountContactsGrid_i').InlinePresenceLookupUIBehavior;
                inlineBehaviour.setDefaultView('927E6CD8-B3ED-4C20-A154-B8BD8A86D172');
                inlineBehaviour.set_disableInlineLookup(true);
        }
    };
    
    this.NYR = NYR;
})();

Notice that we are using the name of the subgrid and the GUID of the required view. After uploading this script as a javascript webresource, we have to change the behavior of the “Add Existing” button using Ribbon Workbench. Change it to call our NYR.addExistingContactSubgrid function. In this case my webresource name is nyr_contact_ribbon.js.

Finally, publish the changes using Ribbon Workbench. Now, when you try to add contact from the subgrid in the account entity, Contacts: No Campaign Activities in Last 3 Months will be the default view.

I have used this to control the inline lookup behavior on a 1:M relationship, but the same approach can be followed for a M:M relationship as well. Notice that I have also hardcoded the GUID of the view. You can use SOAP/OData to retrieve the viewid using the viewname, to improve this code. This is an unsupported customisation, so please use it at your own risk.