Showing posts with label WF. Show all posts
Showing posts with label WF. Show all posts

Thursday, May 13, 2010

MSCRM 4.0: Offset CRM Dates in Workflow without Being Limited

When using Microsoft Dynamics CRM workflow designer tool, you will be limited to a maximum of 31 days (as shown below) if you ever need an offset date based on another CRM date. The same limitation also apply to months, the maximum offset of months is 36 months.
CRM Date Offset Limitation
In order to overcome the limit, I have come up two custom workflow activity classes which let you create any offset date, using the workflow base class that I just created.

1. InvariantOffsetDays
using System;
using System.Globalization;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;

namespace CrmSpikes.Workflow
{
    /// 
    /// Calculate a series of dates using a base date with a set of predefined offset numbers. 
    /// @author Daniel Cai, http://danielcai.blogspot.com/
    /// 
    [CrmWorkflowActivity("Invariant Offset Days", "Custom Workflow")]
    public class InvariantOffsetDays : SingleActivityBase
    {
        private DateTime _baseDate;

        #region Workflow Parameters

        public static DependencyProperty BaseDateProperty =
            DependencyProperty.Register("BaseDate", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmInput("Base Date")]
        public CrmDateTime BaseDate
        {
            get
            {
                return (CrmDateTime)GetValue(BaseDateProperty);
            }
            set
            {
                SetValue(BaseDateProperty, value);
            }
        }

        public static DependencyProperty Offset7DaysProperty =
            DependencyProperty.Register("Offset7Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("7 Days after the base date")]
        public CrmDateTime Offset7Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset7DaysProperty);
            }
            set
            {
                SetValue(Offset7DaysProperty, value);
            }
        }

        public static DependencyProperty Offset14DaysProperty =
            DependencyProperty.Register("Offset14Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("14 Days after the base date")]
        public CrmDateTime Offset14Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset14DaysProperty);
            }
            set
            {
                SetValue(Offset14DaysProperty, value);
            }
        }

        public static DependencyProperty Offset21DaysProperty =
            DependencyProperty.Register("Offset21Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("21 Days after the base date")]
        public CrmDateTime Offset21Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset21DaysProperty);
            }
            set
            {
                SetValue(Offset21DaysProperty, value);
            }
        }

        public static DependencyProperty Offset28DaysProperty =
            DependencyProperty.Register("Offset28Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("28 Days after the base date")]
        public CrmDateTime Offset28Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset28DaysProperty);
            }
            set
            {
                SetValue(Offset28DaysProperty, value);
            }
        }

        public static DependencyProperty Offset35DaysProperty =
            DependencyProperty.Register("Offset35Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("35 Days after the base date")]
        public CrmDateTime Offset35Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset35DaysProperty);
            }
            set
            {
                SetValue(Offset35DaysProperty, value);
            }
        }

        public static DependencyProperty Offset42DaysProperty =
            DependencyProperty.Register("Offset42Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("42 Days after the base date")]
        public CrmDateTime Offset42Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset42DaysProperty);
            }
            set
            {
                SetValue(Offset42DaysProperty, value);
            }
        }

        public static DependencyProperty Offset49DaysProperty =
            DependencyProperty.Register("Offset49Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("49 Days after the base date")]
        public CrmDateTime Offset49Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset49DaysProperty);
            }
            set
            {
                SetValue(Offset49DaysProperty, value);
            }
        }

        public static DependencyProperty Offset56DaysProperty =
            DependencyProperty.Register("Offset56Days", typeof(CrmDateTime), typeof(InvariantOffsetDays));
        [CrmOutput("56 Days after the base date")]
        public CrmDateTime Offset56Days
        {
            get
            {
                return (CrmDateTime)GetValue(Offset56DaysProperty);
            }
            set
            {
                SetValue(Offset56DaysProperty, value);
            }
        }

        #endregion

        #region SequenceActivity

        protected override void ExecuteBody()
        {
            _baseDate = BaseDate.UniversalTime;

            Offset7Days = CalculateOffsetDate(7);
            Offset14Days = CalculateOffsetDate(14);
            Offset21Days = CalculateOffsetDate(21);
            Offset28Days = CalculateOffsetDate(28);
            Offset35Days = CalculateOffsetDate(35);
            Offset42Days = CalculateOffsetDate(42);
            Offset49Days = CalculateOffsetDate(49);
            Offset56Days = CalculateOffsetDate(56);
        }

        private CrmDateTime CalculateOffsetDate(int offset)
        {
            DateTime resultDate = _baseDate.AddDays(offset);
            return CrmDateTime.FromUniversal(resultDate);
        }

        #endregion
    }
}
After you have registered the workflow assembly, and added the custom workflow activity to your workflow by providing a base date, you can then access the generated offset dates as shown below:
InvariantOffset
InvariantOffsetDays class uses a set of predefined offset numbers to generate a series of offset dates based on the provided base CRM date. The offset is hard-coded in the class due to the way how workflow activity works. You may change it to any intervals or any combination of offset numbers.

2. VariantOffsetDays class
using System;
using System.Globalization;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;

namespace CrmSpikes.Workflow
{
    /// <summary>
    /// Calculate a new date using a base date and an offset number (positive or negative). 
    /// @author Daniel Cai, http://danielcai.blogspot.com/
    /// </summary>
    [CrmWorkflowActivity("Variant Offset Days", "Custom Workflow")]
    public class VariantOffsetDays : SingleActivityBase
    {
        #region Workflow Parameters

        public static DependencyProperty BaseDateProperty =
            DependencyProperty.Register("BaseDate", typeof(CrmDateTime), typeof(VariantOffsetDays));
        [CrmInput("Base Date")]
        public CrmDateTime BaseDate
        {
            get
            {
                return (CrmDateTime)GetValue(BaseDateProperty);
            }
            set
            {
                SetValue(BaseDateProperty, value);
            }
        }

        public static DependencyProperty OffsetProperty =
            DependencyProperty.Register("Offset", typeof(CrmNumber), typeof(VariantOffsetDays));
        [CrmInput("Offset (Positive or Negative)")]
        public CrmNumber Offset
        {
            get
            {
                return (CrmNumber)GetValue(OffsetProperty);
            }
            set
            {
                SetValue(OffsetProperty, value);
            }
        }

        public static DependencyProperty ResultDateProperty =
            DependencyProperty.Register("ResultDate", typeof(CrmDateTime), typeof(VariantOffsetDays));
        [CrmOutput("Result Date")]
        public CrmDateTime ResultDate
        {
            get
            {
                return (CrmDateTime)GetValue(ResultDateProperty);
            }
            set
            {
                SetValue(ResultDateProperty, value);
            }
        }

        #endregion

        #region SequenceActivity

        protected override void ExecuteBody()
        {
            DateTime baseDate = BaseDate.UniversalTime;
            DateTime resultDate = baseDate.AddDays(Offset.Value);
            ResultDate = CrmDateTime.FromUniversal(resultDate);
        }

        #endregion
    }
}
VariantOffsetDays class accepts two parameters, which are the base date, and the offset days (int, either positive or negative), as shown below. It’s a more flexible solution than InvariantOffsetDays, the trade-off is it can only generate one offset date at one time.
VariantOffset

Both classes can be used to create any type of offset, including month-based offset or year-based offset, which is your call. 

The reason that I wrote this blog post was that two persons asked similar questions within a month on CRM Development Forum and CRM Forum about how to specify an offset date for a CRM workflow that cannot be done using the native CRM workflow design tool. There doesn't seem to be any available solution on Internet to address this issue, so I decided to write the custom workflow along with the workflow base class.

Note: Please make sure to include the workflow base class that I created in order to compile the code.

Download the source code and compiled assembly below.




Hope this helps if you ever need to do the same thing.

MSCRM 4.0: A Convenience Workflow Base Class

When writing CRM workflow code, you often need to look up some information from its execution context, or instantiate CRM Web Service from the context. If your workflow is complex, you might have more than one method in your workflow activity class, in which case, you are most likely going to pass around the execution context object or the instance of CRM Web Service from one method to another, which is really tedious.

I have come up a convenience workflow base class which might help you become more proficient when developing your custom workflow activity code.

using System;
using System.Workflow.Activities;
using Microsoft.Crm.Workflow;
using System.Workflow.ComponentModel;
using Microsoft.Crm.Sdk;
using System.Web.Services.Protocols;

namespace CrmSpikes.Workflow
{
    /// <summary>
    /// Base class for all Sequence Workflow Activities which consist of only a single workflow activity.
    /// @author Daniel Cai, http://danielcai.blogspot.com/
    /// </summary>
    public abstract class SingleActivityBase : SequenceActivity
    {
        private ICrmService _crmServiceHandle;
        private IMetadataService _metadataServiceHandle;

        #region Base class members

        public IWorkflowContext Context { get; set; }

        /// <summary>
        /// CRM Web Services, instantiated on-demand.
        /// </summary>
        public ICrmService CrmServiceHandle
        {
            get
            {
                _crmServiceHandle = _crmServiceHandle ?? Context.CreateCrmService();
                return _crmServiceHandle;
            }
        }

        /// <summary>
        /// CRM Metadata Service, instantiated on-demand.
        /// </summary>
        public IMetadataService MetadataServiceHandle
        {
            get
            {
                _metadataServiceHandle = _metadataServiceHandle ?? Context.CreateMetadataService();
                return _metadataServiceHandle;
            }
        }
        #endregion

        #region SequenceActivity

        /// <summary>
        /// Workflow step.
        /// </summary>
        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            try
            {
                IContextService _contextService = (IContextService) executionContext.GetService(typeof (IContextService));
                Context = _contextService.Context;

                ExecuteBody();
            }
            catch (SoapException ex)
            {
                string errMsg = string.Format("{0}: {1}", ex, ex.Detail.InnerText);
                throw new InvalidPluginExecutionException(errMsg);
            }
            catch (Exception ex)
            {
                string errMsg = string.Format("{0}: {1}", ex, ex.Message);
                throw new InvalidPluginExecutionException(errMsg);
            }
            finally
            {
                if (_crmServiceHandle != null)
                    _crmServiceHandle.Dispose();

                if (_metadataServiceHandle != null)
                    _metadataServiceHandle.Dispose();

                Context = null;
            }

            return ActivityExecutionStatus.Closed;
        }

        /// <summary>
        /// Overridden in derived classes: contains the actual workflow step.
        /// </summary>
        protected abstract void ExecuteBody();

        #endregion
    }
}

To take advantage of the base class, your workflow class should inherit from the above SingleActivityBase class, such as this one.

using Microsoft.Crm.Workflow;

namespace CrmSpikes.Workflow
{
    [CrmWorkflowActivity("My Custom Workflow", "Custom Workflow")]
    public class MyCustomWorkflow : SingleActivityBase
    {
        #region Workflow Parameters
        #endregion

        #region SequenceActivity

        protected override void ExecuteBody()
        {
            // Do my stuff
        }

        #endregion
    }
}

The benefits of using the convenient base class are:

  1. You can use the base class’s Context property to access the your workflow execution context anywhere in your activity class that you inherit from the base class.
  2. You can use the base class’s CrmServiceHandle property to make CRM service calls right away. The base class takes care of instantiating (and also disposing) CRM Web Service in an on-demand fashion.
  3. Similar to the above, you can use the base class’s MetadataServiceHandle property to make CRM metadata service calls right away. The base class takes care of instantiating (and also disposing) CRM Metadata Service in an on-demand fashion as well.
  4. The base class tries to handle exceptions in a consistent way.

The idea was originally inspired by one of open source projects on CodePlex.com site, but I don't remember which project it is. However, the code here is pretty much a complete rewrite from the scratch. If you happen to know the project, please let me know, I will include a link here to the original project. :-)

It’s not rocket science, but it should make your CRM workflow development a little more convenient. Of course, if you have even better idea about the base class, I would appreciate to have your comments here.

Cheers!