Tuesday, June 25, 2019

How to create a recurring workflow in dynamic 365 CRM?


Business Problem Statement

There was a need to mark all Open Clinic Appointment in a completed state at the end of every day. This is by government law that once a prescription or an advice is generated by doctor for a specific customer, no further modification are allowed.

Technical Options

We can achieve this by having a wait condition in workflow for all open clinic appointment and after certain interval, we can auto close them. But imagine how many workflow instances might be running from the date when clinic appointment got created into the system.

Then I came to know about a blog post https://alexanderdevelopment.net/post/2013/05/18/scheduling-recurring-dynamics-crm-workflows-with-fetchxml having a smart mechanism to schedule a recurring workflow, which is also very easy to configure.

Solution

Here, I will showcase the configuration steps for deactivating the Clinic Appointment daily. 
This involves following components

  • A child workflow to Close a Clinic Appointment in completed state.
  • A Custom entity (Recurring Process) which contains workflow scheduling details and a fetchXml.
  • A Custom Workflow (.Net Code) which is uses this configuration data to fetch actual clinic appointments and execute the child workflow to mark appointment close. 
  • A workflow which will run daily and get trigger and registered on custom entity (Recurring Process)

1) Create a Child Workflow to close or complete the clinic appointment.




2) Next is to configure a custom entity (Recurring Process) with scheduling details and FetchXml for applicable records. (Get installed from solution provided or you can create such custom entity.



3) A workflow (Recurring Workflow Runner) to run on a recurring schedule.


Click on Set Properties for RunScheduledWorkflows

Click on Set Properties – Update Recurring Process for Frequency equals Hourly.


Last, the custom workflow (RunScheduledWorkflow) get installed from the Alex Solution. The code of the workflow is also shared to public.


using System;
using System.Activities;
using System.ServiceModel;
//using System.Globalization;
//using System.Runtime.Serialization;
using System.IO;
using System.Text;
//using System.Net;
using System.Xml;
using Microsoft.Xrm.Sdk;
using Microsoft.Crm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Workflow;
using Microsoft.Xrm.Sdk.Query;
using System.Linq;

namespace Company.Workflows
{
public sealed class RunScheduledWorkflows : CodeActivity
{
    [RequiredArgument]
    [Input("FetchXML query")]
    public InArgument FetchXMLQuery { get; set; }

    [RequiredArgument]
    [Input("Workflow")]
    [ReferenceTarget("workflow")]
    public InArgument Workflow { get; set; }

    [RequiredArgument]
    [Input("Page size")]
    public InArgument PageSize { get; set; }

    //name of your custom workflow activity for tracing/error logging
    private string _activityName = "RunRecurringProcess";

    ///

    /// Executes the workflow activity.
    ///

    /// executionContext">The execution context.
    protected override void Execute(CodeActivityContext executionContext)
    {
        // Create the tracing service
        ITracingService tracingService = executionContext.GetExtension();

        if (tracingService == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve tracing service.");
        }

        tracingService.Trace("Entered " + _activityName + ".Execute(), Activity Instance Id: {0}, Workflow Instance Id: {1}",
            executionContext.ActivityInstanceId,
            executionContext.WorkflowInstanceId);

        // Create the context
        IWorkflowContext context = executionContext.GetExtension();

        if (context == null)
        {
            throw new InvalidPluginExecutionException("Failed to retrieve workflow context.");
        }

        tracingService.Trace(_activityName + ".Execute(), Correlation Id: {0}, Initiating User: {1}",
            context.CorrelationId,
            context.InitiatingUserId);

        IOrganizationServiceFactory serviceFactory = executionContext.GetExtension();
        IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

        try
        {
            //create empty entitycollection for later
            EntityCollection recordsToProcess = new EntityCollection();

            // set page size
            int fetchCount = PageSize.Get(executionContext);

            // Initialize the page number.
            int pageNumber = 1;

            // Specify the current paging cookie. For retrieving the first page,
            // pagingCookie should be null.
            string pagingCookie = null;

            //execute query and page through results
            while (true)
            {
                // Build fetchXml string with the placeholders.
                string fetchXml = CreateXml(FetchXMLQuery.Get(executionContext), pagingCookie, pageNumber, fetchCount);

                EntityCollection retrieved = service.RetrieveMultiple(new FetchExpression(fetchXml));

                //add retrieved records to our main entitycollection
                recordsToProcess.Entities.AddRange(retrieved.Entities);

                if (retrieved.MoreRecords)
                {
                    // Increment the page number to retrieve the next page.
                    pageNumber++;

                    // Set the paging cookie to the paging cookie returned from current results.                           
                    pagingCookie = retrieved.PagingCookie;
                }
                else
                {
                    // If no more records in the result nodes, exit the loop.
                    break;
                }
            }

            //loop through results
            recordsToProcess.Entities.ToList().ForEach(a =>
            {
                ExecuteWorkflowRequest request = new ExecuteWorkflowRequest
                {
                    EntityId = a.Id,
                    WorkflowId = (Workflow.Get(executionContext)).Id
                };

                service.Execute(request);  //run the workflow
            });


        }
        catch (FaultException e)
        {
            tracingService.Trace("Exception: {0}", e.ToString());

            // Handle the exception.
            throw;
        }
        catch (Exception e)
        {
            tracingService.Trace("Exception: {0}", e.ToString());
            throw;
        }

        tracingService.Trace("Exiting " + _activityName + ".Execute(), Correlation Id: {0}", context.CorrelationId);
    }

    ///

    /// used to enable paging in the fetchxml queries - https://msdn.microsoft.com/en-us/library/gg328046.aspx
    ///

    /// xml">
    /// cookie">
    /// page">
    /// count">
    ///
    public string CreateXml(string xml, string cookie, int page, int count)
    {
        StringReader stringReader = new StringReader(xml);
        XmlTextReader reader = new XmlTextReader(stringReader);

        // Load document
        XmlDocument doc = new XmlDocument();
        doc.Load(reader);

        return CreateXml(doc, cookie, page, count);
    }

    ///

    /// used to enable paging in the fetchxml queries - https://msdn.microsoft.com/en-us/library/gg328046.aspx
    ///

    /// doc">
    /// cookie">
    /// page">
    /// count">
    ///
    public string CreateXml(XmlDocument doc, string cookie, int page, int count)
    {
        XmlAttributeCollection attrs = doc.DocumentElement.Attributes;

        if (cookie != null)
        {
            XmlAttribute pagingAttr = doc.CreateAttribute("paging-cookie");
            pagingAttr.Value = cookie;
            attrs.Append(pagingAttr);
        }

        XmlAttribute pageAttr = doc.CreateAttribute("page");
        pageAttr.Value = System.Convert.ToString(page);
        attrs.Append(pageAttr);

        XmlAttribute countAttr = doc.CreateAttribute("count");
        countAttr.Value = System.Convert.ToString(count);
        attrs.Append(countAttr);

        StringBuilder sb = new StringBuilder(1024);
        StringWriter stringWriter = new StringWriter(sb);

        XmlTextWriter writer = new XmlTextWriter(stringWriter);
        doc.WriteTo(writer);
        writer.Close();

        return sb.ToString();
    }
}

}

Thanks Alex, for sharing such a good workflow logic.

Regards,
Vipin Jaiswal
vipinjaiswal12@gmail.com

Monday, June 24, 2019

The async operation was skipped because the org is in “Disable Background Processing” mode.



I was getting such an error when trying to run a workflow in a Sandbox environment.

Sandbox environment was exclusively made a copy of Production.

Solution
To enable background processing again. 
Visit CRM Admin Portal and Select your Instance and click on Admin Settings as shown below.



Untick the Disable background operations


Thursday, June 13, 2019

How to create a Custom Button in Dynamic CRM and invoke a JavaScript method


Creating a button requires an image with following
  • correct size 16 X 16
  • which can denote a typical functionality
  • should be in-line with other icons in Dynamic CRM Theme.
  • It is easy and good option to do all configuration via Ribbon Workbench Tool.

Finding a Perfect Icons.
Usually we can google an icon based on the functionality we are interested in. 
However, I came to learn about these websites which provide free cool icons.

Making icon Right Size
For button on a form it should be of 16 X 16 pixels.
For Icon in Navigation it should be of 32 X 32 pixels.
Here is a online image resizer I often use - https://resizeimage.net/
Once you upload an image, just go to 4th Step and enter pixel size accordingly.


Next jump to Step 7 and hits the RESIZE button and download your image.

Creating a Button using Ribbon Workbench
1) Create a web-resource of your new image.

2) Create a Solution
It is suggested to have a solution with only relevant entities where we are interested in adding a button. We should not overload a solution with too many entities or other components, as it might affect the loading and publishing of a solution from a Ribbon Workbench.

Working the Ribbon workbench


Start with selecting an Entity.
  1. Drag a button from Toolbox to the form section or any section where you need it.
  2. Select the button to enable to button properties.
  3. Provide button properties like Label, Tool tips, etc.
  4. Select you image from the web-resource created.

  1. Jump to Command Section and add the command
  2. Rename the command with appropriate name that denote the required functionality
  3. If you want to pass any parameter – add parameter.

!!! Now Do not forget to bind your command to the button !!!


Hit the publish button on Ribbon workbench and wait for a while.
Finally, to see the outcome use Hard Refresh (CTRL + F5) to reload dynamic CRM.



Regards,
Vipin Jaiswal
vipinjaiswal12@gmail.com