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!

Advertisements

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.