SetStateRequest deprecation and plugin steps

With the SetStateRequest message now being deprecated you may find yourself starting to amend any code to use an Update request instead. Please note though that any plugins steps registered on SetStateRequest will not subsequently trigger on update of state when using an Update request. Therefore don’t forget to update your plugins steps at the same time!

Email Dynamics 365 team using Microsoft Flow

This week I had a requirement to send a daily email containing a list of open leads to different Dynamics teams based on the country of the lead.  Rather than diving straight in writing a custom workflow activity to do so I thought I’d give it a go using Flow.  Most of the steps were fairly self-explanatory but retrieving the team members to email required a bit of a workaround.  Here’s how to do it:

    1. Create a schedule step01_Schedule
    2. Initialize a variable to store our team member email address in (more on why this is required later)07_Initialise_Variable
    3. Give this a suitable name and ensure it’s of type String08_Variable_Definition
    4. Retrieve the Dynamics team using Dynamics 365 – List Records (Preview) step.  Be sure to Show advanced options for this step.  To retrieve team members you would usually use the teammemberships entity.  However you will notice that this is not listed.  02_List_Records_Team_MissingFear not, scroll right to the bottom and select Enter custom value.  Once done, type in teammemberships.  You’ll also want to add a filter in to retrieve only the desired team.  In this example I have used the guid of the team in the filter.03_List_Records_Custom_Value
    5. Here comes the workaround:  Because teammemberships wasn’t in the Entity list in the previous step, Flow doesn’t know how to handle the response.  Put another way, it doesn’t know what fields the teammembership entity contains.  Fortunately Flow uses the Web API so we can make use of the Parse JSON step to extract the info we need from the response.04_Parse_JSON_Step
    6. Once selected, select the List of Items from the Previous step05_List_of_Items
    7. Before moving on we must enter the schema that is used in the response from the Retrieve Dynamics Team Member step.  Fortunately this can be entered by passing in an example response with Flow then working out the schema to use.  Alternatively copy and paste the below:
      {
          "type": "object",
          "properties": {
              "@@odata.context": {
                  "type": "string"
              },
              "value": {
                  "type": "array",
                  "items": {
                      "type": "object",
                      "properties": {
                          "@@odata.etag": {
                              "type": "string"
                          },
                          "ItemInternalId": {
                              "type": "string"
                          },
                          "systemuserid": {
                              "type": "string"
                          },
                          "versionnumber": {
                              "type": "integer"
                          },
                          "teammembershipid": {
                              "type": "string"
                          },
                          "teamid": {
                              "type": "string"
                          }
                      },
                      "required": [
                          "@@odata.etag",
                          "ItemInternalId",
                          "systemuserid",
                          "versionnumber",
                          "teammembershipid",
                          "teamid"
                      ]
                  }
              }
          }
      }
      
    8. Next add a Apply to each step, using the value of Parse JSON step to loop through06_Apply_To_Each
    9. For each team member returned we now need to retrieve that user’s email address from the user entity.  For this we’ll use the Dynamics 365 – Get record (Preview) step09_Get_Record
    10. Fortunately this time Users is selectable.  For the Item identifier we’ll pass in the systemuserid from the Parse JSON step10_Get_User
    11. Still inside our Apply to each step add a Variables – Append to string variable action11_Append_To_String
    12. Select the name of the variable you created earlier and in the value12_Add_Primary_Email
    13. Add the same step again but this time set the value as a semi-colon ;13_Semi_Colon
    14. Come outside of the Apply to each step and add an Office 365 Outlook – Send an email action14_Send_Email_Step
    15. Click See more listed under Variables 15_See_More
    16. Finally, add the Team Member variable we created earlier to the To field and enter the content of the email.

 

 

 

FetchXML – selecting no attributes gives you all attributes

I recently encountered a problem with a query inside a scheduled process timing out and the process failing as a result.  The process itself bulk reassigns records based on given criteria, occasionally hitting the 10,000 record mark for each entity.  In order to assign the records we only required the entity reference itself, none of the entity attributes.  As such the query looked similar to the below.  Note the lack of attributes, just a filter.

<fetch>
  <entity name="opportunity" >
    <filter>
      <condition attribute="createdon" operator="last-x-months" value="1" />
    </filter>
  </entity>
</fetch>

As part of the debugging process I copied this query into the FetchXml Builder in the XrmToolBox and to my surprise also encountered a timeout error.  Limiting the query to the top 10

<fetch top="10" >

eventually returned 10 opportunities and all of their attributes.

As this was running against the opportunity it tried to return 250 attributes for 5000 records in each query, thus causing a timeout.  I’m not sure if this has always been the case or just as the result of upgrading to the latest 365 DLLs from the SDK as previously we would use the <all-attributes> tag.

<fetch >
  <entity name="opportunity" >
    <all-attributes/>
    <filter>
      <condition attribute="createdon" operator="last-x-months" value="1" />
    </filter>
  </entity>
</fetch>

Anyway, even if you don’t have use of any of the attributes it seems it would be advised to request at least one to avoid causing unnecessary timeouts.

<fetch top="10" >
  <entity name="opportunity" >
    <attribute name="opportunityid" />
    <filter>
      <condition attribute="createdon" operator="last-x-months" value="1" />
    </filter>
  </entity>
</fetch>

Add additional fields to Dynamics CRM/365 Excel Template

Recently I was asked to put together a sales forecast Excel Template to include a few pivot tables, charts and sliders to analyse our open opportunities.  Took a couple of hours to get it right after which I presented it back to the team.  “Looks good, Luke.  You couldn’t just add in an extra field though, could you?”.  Alas, once you’ve made your initial selection of fields your only obvious option is to start again from scratch – once you have exported the initial template there is no way to add additional fields through the UI.  Due to the time taken to produce I thought surely there must be an easier and less time consuming way.  Fortunately there is.

On every Excel template there is a hidden sheet, aptly named hiddenDataSheet.  In this hidden sheet you may find the CRM query in cell A1, which you can edit.  To view this hidden sheet you’ll need to open the sheet’s VBA view (Alt+F11).  Once there you’ll see the hiddenDataSheet listed.

lsd_hiddenDataSheet

In the properties for this sheet the Visible property is set to 2 – xlSheetVeryHidden.  Change this property to -1 – SheetVisible and close the VBA window.  Once done you’ll see a new tab at the bottom of your Excel Template.

lsd_hiddenDataSheet_tab

In cell A1 of this worksheet you will find the query.  I found it easier at this point to copy and paste it into a text editor to decipher it.

lsd_templatequery

The important bit to note from this query is that each field has a format like &crmfieldname=Spreadsheet%20Column

The %20 in the above snippet represents a space in the Excel field name.  To add a new field we simply need to create the field first in our worksheet and then add it on to the end of the query.  For example if we wanted a new column for Status Reason we would add:

&statuscode=Status%20Reason

Adding fields from related entities are a lit bit more tricky as the CRM field name is proceeded with a guid for the relationship.  You will need another field already from that entity in your template in order to copy this guid to add before the new field.

Once done paste your new query back into cell A1.  Then hit Alt+F11 again and set the hiddenDataSheet Visible back to 2 – xlSheetVeryHidden, close the VBA window, save your document and reupload*.

*There is a cracking XrmToolBox plugin called Document Template Manager that will help here.

 

 

Include entity metadata in solutions to avoid entity audit being disabled

Since upgrading to Dynamics CRM 2016 I have made great use of only adding the entity components required to solutions rather than the entire entity itself.  The key reason why you would want to do this is to ensure that no unintended changes are made in the target system.

Recently upon importing an unmanaged solution it was noted that entity audit for one of the entities contained in the solution was temporarily disabled and re-enabled at the time of import.  I’ve seen this happen with managed solutions before but never unmanaged ones.  This shouldn’t be much of an issue but a known bug in CRM means that despite the audit logs remaining present all historic changes are lost, rendering the audit log completely useless.  A post about this may be found here.

Upon further investigation as to how this happened, and with a bit of trial and error, it would appear that this issue is caused by not including entity metadata in the solution.

lsd_entity_metadata

One imagines that this is a bug that will be fixed at some point but in the meantime it’s probably worth including it in to avoid losing your audit history.  For more information about what is contained in the entity metadata, please visit Ben Hosking’s blog on it here.

this will break your phone app scripts

When writing JavaScript for CRM forms I like to encase all form functions within a namespace library in a very similar fashion to what Microsoft suggest here. It ends up looking like follows.

//If the LSD namespace object isn’t defined, create it.
if (typeof (LSD) == "undefined")
 { LSD = {}; }
  // Create Namespace container for functions in this library;
  LSD.Appointment= {
   onLoad: function(){
    this.retrieveLocation();
  },
  retrieveLocation: function(){
   console.log("retrieving contact address");
  }
};

Recently I rewrote some form scripts for appointment to add additional functionality, replaced web service calls using the 2011 Endpoint with the WebAPI and also took the opportunity to create a namespaced library.  I tested the changes in Dev and UAT before finally deploying to live.

Today I received a phonecall from one of the sales guys who informed me I had broken the phone app.

lsd_phoneapp_error

Oops, I had forgot to test the script changes on the app.  Had I have done so I would have discovered that you cannot use the this keyword to call functions within the namespace when using the phoneapp, you must use the full name as shown below.

//If the LSD namespace object isn’t defined, create it.
if (typeof (LSD) == "undefined")
 { LSD = {}; }
  // Create Namespace container for functions in this library;
  LSD.Appointment= {
   onLoad: function(){
    LSD.Appointment.retrieveLocation();
  },
  retrieveLocation: function(){
   console.log("retrieving contact address");
  }
};

So there are a few lessons there:

  1. Never use this if you intend on using the CRM phone app
  2. Always, always test your scripts in the phone app before deploying to live.

Customer field type query enhancement using a RetrieveMultiple plugin

In a previous post I discussed some of the issues and limitations when using the Customer field type.  One of the issues I highlighted was that when viewing Recent Cases on the Contact it would only return Cases where the Contact was set as the Customer, not as the Primary Contact.  Where the Contact could be listed as the Customer or Case Primary Contact two subgrids would be required (one for each relationship).

Now that I have finally emptied my fridge of leftover beer from Christmas I thought I would investigate whether it would be possible to display both of these in the same subgrid using a plugin on RetrieveMultiple of Case to intercept and amend the query before it is executed.  It turns out it is indeed possible.

This will of course work for any entity that uses the Customer field type, not just Case.  To use this for other entities simply register the plugin on RetrieveMultiple of the required entity (step 2).

Please note that the code below has little of no error handling in and was simply a proof of concept.  It is certainly not ready for any production environments but will hopefully give others a starting point.

Steps to achieve this:

  1. Create a new plugin
    using Microsoft.Xrm.Sdk;
    using Microsoft.Xrm.Sdk.Query;
    using System;
    
    namespace LSD.Plugins.RetrieveMultiple
    {
        public class CaseQueryRetrieveMultiple : IPlugin
        {
            #region Secure/Unsecure Configuration Setup
            private string _secureConfig = null;
            private string _unsecureConfig = null;
    
            public CaseQueryRetrieveMultiple(string unsecureConfig, string secureConfig)
            {
                _secureConfig = secureConfig;
                _unsecureConfig = unsecureConfig;
            }
            #endregion
            public void Execute(IServiceProvider serviceProvider)
            {
                ITracingService tracer = (ITracingService)serviceProvider.GetService(typeof(ITracingService));
                IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
                IOrganizationServiceFactory factory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                IOrganizationService service = factory.CreateOrganizationService(context.UserId);
    
                try
                {
                    if(context.InputParameters.Contains("Query"))
                    {
                        QueryExpression objQueryExpression = (QueryExpression)context.InputParameters["Query"];                   
    
                        FilterExpression customerFilter = new FilterExpression(LogicalOperator.Or);
                        FilterExpression otherFilters = new FilterExpression(LogicalOperator.And);
                        FilterExpression combinedFilters = new FilterExpression(LogicalOperator.And);
    
                        foreach (ConditionExpression condition in objQueryExpression.Criteria.Conditions)
                        {
                            if(condition.AttributeName == "customerid")
                            {
                                var recordId = condition.Values[0];
                                customerFilter.Conditions.Add(new ConditionExpression("customerid", ConditionOperator.Equal, recordId));
                                customerFilter.Conditions.Add(new ConditionExpression("primarycontactid", ConditionOperator.Equal, recordId));
                            }
                            else
                            {
                                otherFilters.Conditions.Add(condition);
                            }
                        }
                        combinedFilters.AddFilter(customerFilter);
                        combinedFilters.AddFilter(otherFilters);
                        objQueryExpression.Criteria = combinedFilters;
                    }
    
                }
                catch (Exception e)
                {
                    throw new InvalidPluginExecutionException(e.Message);
                }
            }
        }
    }
    
    
  2. Register this on Pre-operation of RetrieveMultiple on the incident entitylsd_plugin_retrievemultiple_case
  3. Update the Step and View your subgrid

lsd_plugin_retrievemultiple_subgrid

Now you can just display the one subgrid without missing any vital information.

Reassign Security Roles on change of Business Unit

Back when I worked for a CRM partner, and in my early days of CRM development, I would often get asked how long something would take to code – a “ballpark figure” – usually for a small requirement or change request.   Invariably I would have a quick look and give an educated guess with a few caveats.  Back then I perhaps was a bit out with some of my estimates but generally I was within an acceptable margin of error.  However on occasions I am completely wide of of mark.  And last week was such an occasion.

I needed to move around 30 users to different Business Units.  As we know Security Roles are dropped when users move Business Unit.  Unfortunately the average number of Security Roles for each of these users was probably around 6.  The prospect of manually reassigning these didn’t exactly fill me with joy (somewhere on my mid/long term project list I’m sure I have a task to simply make users members of Teams and assign the roles to the team instead – a lot less cumbersome).  Fortunately I came up with a temporary plan.

  1. Write a plugin on update of a user’s Business Unit
  2. Execute on Pre-Operation stage to generate a list of RoleIds that the user has before they change Business Unit
  3. Add the the list of RoleIds to the plugin Shared Variables 
  4. Grab these RoleIds from Shared Variables in the post-operation stage and then reassign

Simple.  Can get that done in an hour tops.

Problems with the above:

  1. Security Roles are dropped prior to pre-operation and the plugin would not execute pre-validation
  2. Security Roles have a different RoleId for each Business Unit so attempting to reassign a Security Role from a different BU resorts in an error

4 hours later here’s the solution that actually worked:

  1. Use an on-demand custom workflow activity to retrieve all of the user’s role names (Not RoleIds!).  Write this list of role names as a comma separated list to store in a hidden text field on the user record
  2. Change the user’s Business Unit
  3. Use another on-demand workflow to iterate through each of the roles listed in the aforementioned hidden field.  For each role query the role table filtered by role name and the new Business Unit Id to retrieve the new Role Id
  4. With the retrieved Role Id associate it to the user (code below)
service.Associate(
 "systemuser",
 userId,
 new Relationship("systemuserroles_association"),
 new EntityReferenceCollection() { new EntityReference("systemuserroles", buRole) });

I’ve not got round to tidying the code up yet (stripping out my other gunk) but if anybody is interested in the completed solution drop a message in the comments and I’ll be sure to get round to posting it.  Hopefully save somebody else some time at least.

Add Xrm.Page IntelliSense to Visual Studio

Despite being a CRM developer for almost three years now I still find myself having, on occasions, to refer to the Microsoft Developer Network for common Xrm.Page functions when writing client-side script, usually forgetting whether functions are in Xrm.Page.data or Xrn.Page.context.  Fortunately the guys over at MSXrmTools have put together IntelliSense libraries for both CRM 2013 and 2016 which can be easily referenced in Visual Studio to provide IntelliSense capabilities, and thus reducing the amount of time spent resolving script errors.

To get this working first head over to the MSXrmTools site (or click either 2013 or 2016 above to go directly to the JavaScript library) and save the required JavaScript library somewhere convenient on your local machine (ensuring it’s saved as a .js file).

Next load up Visual Studio.  Once loaded navigate to Tools > Options.  Then when the Options window appears expand Text Editor on the left and then also expand the JavaScript and IntelliSense tabs, before settling on References (as shown below).

lsd_intellisense_options

Once there select Implict (Web) from the Reference Group and then browse using the ellipsis button (…) for the previously saved JavaScript library.  You will then see the Xrm.Page library listed under Included Files.

Now when creating or editing JavaScript or HTML files within Visual Studio you’ll have IntelliSense capabilities.

lsd_intellisense_example1

Full credit to MSXrmTools for putting together the resource.

ClientGlobalContext.js.aspx destroys backspace functionality in HTML text fields

Sounds bonkers but it’s true.  Whilst making some amendments to a custom product picker (HTML web resource) I added a reference to ClientGlobalContext.js.aspx to avoid relying on getting the context from the parent window, which I had some issues with when using turbo forms in Outlook Client.  Shortly after publishing these changes I started to receive reports that the backspace in a HTML input field had stopped deleting text.  Somewhat baffled I headed to Google and stumbled across this post on the community forum stating the problem to be with the aforementioned script.  I must admit that I didn’t believe that that could be the cause of the issue but removed the reference to the script anyway.  Remarkably the backspace started to work again.

As I needed both the script and the backspace working I ended up writing a custom function using jQuery to replicate the backspace functionality.

function keyDown(event) {
            if (event.keyCode === 8) {   //backspace
                var searchValue = $("#search-criteria").val();
                if (searchValue.length > 0) $("#search-criteria").val(searchValue.slice(0, -1));                  

            }
        }

I then added the keyDown function to the input field.

<input type="search" placeholder="Search for product id or name" id="search-criteria" onkeypress="keyPress(event)" autofocus />

I think we can definitely file that one away in the Dynamics idiosyncrasies section.