Tuesday, May 25, 2010

Use CRM Web Service Toolkit to Implement Associate/Disassociate/SetState Functions

There was a comment in my new CRM Web Service Toolkit release page, complaining the following functions were missing from the toolkit when comparing to Ascentium library.

- Associate
- Disassociate
- SetState

I want to make it very clear upfront. It was never my intention to beat anyone or anything by writing the toolkit. I wrote it simply because I had too much pain to write ad-hoc JavaScript functions to make CRM Web Service calls. It was very inefficient, and also error-prone.

If any of you ever care about how and why it happened, here is a bit story behind the toolkit. I started with a few reusable JavaScript functions at the very beginning, without knowing Ascentium library exists (If I knew in the first place, I would probably never started), and gradually made it into a helper utility. To be honest, it took me quite some effort to get there, as I was an amateur JavaScript developer. I liked most of the implementation, but it was too simple, can only do a couple of things. So I decided to make it better, that's how CRM Service Toolkit 1.0 was born, which killed me almost another whole weekend time plus some evening time during that week, it was the time that I came cross Ascentium library, from which I incorporated the self-contained BusinessEntity concept. The toolkit looked a lot better, but since it's my spare time hobby project, I didn't actually bring the toolkit to my work project until almost 3 months later due to time constraint and distraction of other engagements. As soon as I started to use it in my work project, I immediately realized some problems, most significantly, adding toBoolean() prototype function to JavaScript Number object was simply a wrong decision. That's the biggest motivation for me to write a 2.0 version, as I feel obliged that I have to address this bad design. In the meantime, I wanted to incorporate some security features to the toolkit as they are very often used in CRM projects. That's where v2.0 came from. But since I can only do it on my personal time, it took me roughly a month to find some spare time to really focus on the v2.0 enhancements.

Way off-topic, I just want to make it clear about my intention of writing the toolkit.

Back to the topic of those missing functions, I actually thought about introducing them to the toolkit library, but I decided not to do so, in order to keep the toolkit as nimble as possible, I didn't seem to see they are so often used.

However if you ever need those functions, here are the implementations:

Associate and Disassociate Functions

/**
 * Associate two CRM records that have a N:N relationship. 
 * @param {String} relationshipName Name of the many-to-many relationship.
 * @param {String} entity1Name Entitiy name of the first record to be associated.
 * @param {String} entity1Id CRM Record ID (GUID) of the first record to be associated.
 * @param {String} entity2Name Entitiy name of the second record to be associated.
 * @param {String} entity2Id CRM Record ID (GUID) of the second record to be associated.
 * @return {object} The XML representation of the result.
 */
associate = function(relationshipName, entity1Name, entity1Id, entity2Name, entity2Id)
{
    var request = [
"<Request xsi:type='AssociateEntitiesRequest'>",
    "<Moniker1>",
        "<Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity1Id, "</Id>",
        "<Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity1Name, "</Name>",
    "</Moniker1>",
    "<Moniker2>",
        "<Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity2Id, "</Id>",
        "<Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity2Name, "</Name>",
    "</Moniker2>",
    "<RelationshipName>", relationshipName, "</RelationshipName>",
"</Request>"
].join("");

    return CrmServiceToolkit.Execute(request);
};

/**
 * Disassociate two CRM records that have a N:N relationship. 
 * @param {String} relationshipName Name of the many-to-many relationship.
 * @param {String} entity1Name Entitiy name of the first record to be disassociated.
 * @param {String} entity1Id CRM Record ID (GUID) of the first record to be disassociated.
 * @param {String} entity2Name Entitiy name of the second record to be disassociated.
 * @param {String} entity2Id CRM Record ID (GUID) of the second record to be disassociated.
 * @return {object} The XML representation of the result.
 */
disassociate = function(relationshipName, entity1Name, entity1Id, entity2Name, entity2Id) {
    var request = [
"<Request xsi:type='DisassociateEntitiesRequest'>",
    "<Moniker1>",
        "<Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity1Name, "</Name>",
        "<Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity1Id, "</Id>",
    "</Moniker1>",
    "<Moniker2>",
        "<Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity2Name, "</Name>",
        "<Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entity2Id, "</Id>",
    "</Moniker2>",
    "<RelationshipName>", relationshipName, "</RelationshipName>",
"</Request>"
].join("");

    return CrmServiceToolkit.Execute(request);
};

SetState Function

[Update - May 26, 2010] vlad007 left a comment pointing out the request XML was not in right sequence, so I have just updated the script. Thanks vlad007!
/**
 * Set a CRM record's state by its statecode and statuscode. 
 * @param {String} entityName Entitiy name of the CRM record to be updated.
 * @param {String} id CRM Record ID (GUID) to be updated.
 * @param {String} statecode New statecode in string, eg, "Active", "Inactive".
 * @param {Integer} statuscode New statuscode in integer, use -1 for default status.
 * @return {object} The XML representation of the result.
 */
setState = function(entityName, id, stateCode, statusCode) {
    var request = [
"<Request xsi:type='SetStateDynamicEntityRequest'>",
    "<Entity>",
        "<Name xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", entityName, "</Name>",
        "<Id xmlns='http://schemas.microsoft.com/crm/2006/CoreTypes'>", id, "</Id>",
    "</Entity>",
    "<State>", stateCode, "</State>",
    "<Status>", statusCode, "</Status>",
"</Request>"
].join("");

    return CrmServiceToolkit.Execute(request);
};

Please be advised, those functions were not actually unit tested. Please let me know if you have problems using them.

The above code should also be ablet to give you some basic ideas about how to add support of any other CRM messages that you may need to the toolkit.

Cheers!

Sunday, May 23, 2010

Release: MSCRM4 Web Service Toolkit for JavaScript v2.0

[UPDATE - July 4, 2010] A new version has been released at http://danielcai.blogspot.com/2010/07/crm-web-service-toolkit-for-javascript.html, please ensure to check out.

Here is another update of CRM Web Service Toolkit for JavaScript that I released to codeplex site today, most likely this is going to be the last release before CRM5. This new release is based on previous version (v1.0), and it comes with a few more enhancements:
  1. A new method called queryByAttribute() has been added, which allows to retrieve a specific entity's record by using one or more than one pair of attribute and value
  2. Three new methods have been added to help facilitate user and user security role related queries, including getCurrentUserId(), getCurrentUserRoles(), isCurrentUserInRole()
  3. The toBoolean() prototype function that I added to JavaScript Number type in the previous version is now obsolete, instead I have added a new prototype function to the toolkit's BusinessEntity object. So if you want to retrieve a CRM Boolean type field's value, you should use something like this: businessEntity.getValueAsBoolean('new_mybooleanfield')
  4. A new prototype function called getValueAsLookup has been added to the toolkit's BusinessEntity object, which allows you to parse the values of a CRM lookup field that you retrieved through the toolkit and convert it to a CRM lookup control's DataValue. For instance, you could do something like this: crmForm.all.new_mylookup.DataValue = businessEntity.getValueAsLookup("new_mylookup", "new_mylookupentity")
Again, here are a few samples that might help you get started with the toolkit.
  1. Use CrmServiceToolkit.Create() to create a CRM record.
    // Use CrmServiceToolkit. Create() to create a CRM contact record.
    var contact = new CrmServiceToolkit.BusinessEntity("contact");
    contact.attributes["firstname"] = "Diane";
    contact.attributes["lastname"] = "Morgan";
    contact.attributes["gendercode"] = 2;
    contact.attributes["familystatuscode"] = 1; // Picklist : Single - 1
    contact.attributes["creditlimit"] = 3000;
    
    var createResponse = CrmServiceToolkit.Create(contact);
  2. Use CrmServiceToolkit.Update() to update a CRM record.
    //Use CrmServiceToolkit.Update() to update a CRM contact record. 
    var contactId = '3210F2BC-1630-EB11-8AB1-0003AAA0123C';
    var contact = new CrmServiceToolkit.BusinessEntity("contact");
    contact.attributes["contactid"] = contactId;
    contact.attributes["firstname"] = "Diane";
    contact.attributes["lastname"] = "Lopez";
    contact.attributes["familystatuscode"] = 2; // Married
    
    var updateResponse = CrmServiceToolkit.Update(contact);   
  3. Use CrmServiceToolkit.Retrieve() to retrieve a CRM record.
    // Use CrmServiceToolkit.Retrieve() to retrieve a CRM contact record.
    var contactId = '3210F2BC-1630-EB11-8AB1-0003AAA0123C'; 
    var cols = ["firstname", "lastname", "familystatuscode", "creditlimit", "birthdate", "donotemail"];
    var retrievedContact = CrmServiceToolkit.Retrieve("contact", contactId, cols);
    
    alert(retrievedContact.getValue('lastname'));
    alert(retrievedContact.getValue('firstname'));
    alert(retrievedContact.getValue('familystatuscode')); // Picklist's value (integer)
    alert(retrievedContact.getValue('familystatuscode', 'name')); // Picklist's selected text
    alert(retrievedContact.getValue('creditlimit')); // Currency field's value
    alert(retrievedContact.getValue('creditlimit', 'formattedvalue')); // Currency field's formatted value (string)
    alert(retrievedContact.getValue('birthdate')); // Datetime field's date/time value
    alert(retrievedContact.getValue('birthdate', 'date')); // Datetime field's date string
    alert(retrievedContact.getValue('birthdate', 'time')); // Datetime field's time string
    alert(retrievedContact.getValueAsBoolean('donotemail')); // Bit field's value
  4. Use CrmServiceToolkit.RetrieveMultiple() to retrieve a collection of CRM records.
    // Retrieve all contacts whose first name is John. 
    var firstname = 'John'; 
    var query = [
    "<q1:EntityName>contact</q1:EntityName>",
    "<q1:ColumnSet xsi:type='q1:ColumnSet'>",
       "<q1:Attributes>",
          "<q1:Attribute>firstname</q1:Attribute>",
          "<q1:Attribute>lastname</q1:Attribute>",
          "<q1:Attribute>familystatuscode</q1:Attribute>",
          "<q1:Attribute>ownerid</q1:Attribute>",
          "<q1:Attribute>creditlimit</q1:Attribute>",
          "<q1:Attribute>birthdate</q1:Attribute>",
          "<q1:Attribute>donotemail</q1:Attribute>",
       "</q1:Attributes>",
    "</q1:ColumnSet>",
    "<q1:Distinct>false</q1:Distinct>",
    "<q1:Criteria>",
       "<q1:FilterOperator>And</q1:FilterOperator>",
       "<q1:Conditions>",
          "<q1:Condition>",
             "<q1:AttributeName>firstname</q1:AttributeName>",
             "<q1:Operator>Equal</q1:Operator>",
             "<q1:Values>",
                "<q1:Value xsi:type='xsd:string'>", firstname, "</q1:Value>",
             "</q1:Values>",
          "</q1:Condition>",
       "</q1:Conditions>",
    "</q1:Criteria>"
    ].join("");
    
    var retrievedContacts = CrmServiceToolkit.RetrieveMultiple(query);
    
    alert(retrievedContacts.length);
    alert(retrievedContacts[0].getValue('lastname'));
    alert(retrievedContacts[0].getValue('firstname'));
    alert(retrievedContacts[0].getValue('familystatuscode');
    alert(retrievedContacts[0].getValue('familystatuscode', 'name'));
    alert(retrievedContacts[0].getValue('creditlimit'));
    alert(retrievedContacts[0].getValue('creditlimit', 'formattedvalue'));
    alert(retrievedContacts[0].getValue('birthdate'));
    alert(retrievedContacts[0].getValue('birthdate', 'date'));
    alert(retrievedContacts[0].getValue('birthdate', 'time'));
    alert(retrievedContacts[0].getValueAsBoolean('donotemail'));
  5. Use CrmServiceToolkit.Fetch() to retrieve a collection of CRM records using FetchXML query.
    // Fetch all contact records whose first name is John using FetchXML query
    var firstname = 'John';
    var fetchXml = [
    "<fetch mapping='logical'>",
       "<entity name='contact'>",
          "<attribute name='contactid' />",
          "<attribute name='firstname' />",
          "<attribute name='lastname' />",
          "<attribute name='familystatuscode' />",
          "<attribute name='ownerid' />",
          "<attribute name='creditlimit' />",
          "<attribute name='birthdate' />",
          "<attribute name='accountrolecode' />",
          "<attribute name='donotemail' />",
          "<filter>",
             "<condition attribute='firstname' operator='eq' value='", firstname, "' />",
          "</filter>",
       "</entity>",
    "</fetch>"
    ].join("");
    
    var fetchedContacts = CrmServiceToolkit.Fetch(fetchXml);
    
    alert(fetchedContacts.length);
    alert(fetchedContacts[0].getValue('lastname'));
    alert(fetchedContacts[0].getValue('firstname'));
    alert(fetchedContacts[0].getValue('familystatuscode');
    alert(fetchedContacts[0].getValue('familystatuscode', 'name'));
    alert(fetchedContacts[0].getValue('creditlimit'));
    alert(fetchedContacts[0].getValue('creditlimit', 'formattedvalue'));
    alert(fetchedContacts[0].getValue('birthdate'));
    alert(fetchedContacts[0].getValue('birthdate', 'date'));
    alert(fetchedContacts[0].getValue('birthdate', 'time'));
    alert(fetchedContacts[0].getValueAsBoolean('donotemail'));
  6. Use CrmServiceToolkit.Delete() to delete a CRM record.
    // Use CrmServiceToolkit.Delete() to delete a CRM contact record. 
    var contactId = '3210F2BC-1630-EB11-8AB1-0003AAA0123C';
    var deleteResponse = CrmServiceToolkit.Delete("contact", contactId);
    alert(deleteResponse);
  7. Use CrmServiceToolkit.Execute() to execute a message.
    // Use CrmServiceToolkit.Execute() to execute a message. 
    var whoAmI = CrmServiceToolkit.Execute("<Request xsi:type='WhoAmIRequest' />");
    currentUserId = whoAmI.getElementsByTagName("UserId")[0].childNodes[0].nodeValue;
    alert("Current user's ID is " + currentUserId);
  8. Use CrmServiceToolkit.queryByAttribute() to retrieve a CRM record using one criterion.
    // Use CrmServiceToolkit.queryByAttribute() to retrieve a set of CRM records.
    var retrievedContacts = CrmServiceToolkit.queryByAttribute("contact", "firstname", "John"); // Retrieve all contacts whose first name is John.
    
    alert(retrievedContacts[0].getValue('lastname'));
    alert(retrievedContacts[0].getValue('firstname'));
    NOTE: In this example, I didn't specify columnSet parameter, so it will return all available fields of the contact entity, which is a really BAD practice. You should always specify what you want to get, if that's possible.

    NOTE: The signature of this method has been changed in v2.1, please refer to the latest release page if you are using v2.1.

  9. Use CrmServiceToolkit.queryByAttribute() to retrieve a CRM record using more than one criterion, with specified column set and sorting order.
    // Use CrmServiceToolkit.queryByAttribute() to retrieve a set of CRM records using more than one criterion with specified column set or sorting order
    var attributes = ["firstname", "lastname"];
    var values = ["John", "Wayne"];
    var cols = ["familystatuscode", "ownerid", "creditlimit", "birthdate", "donotemail", "donotphone"];
    var orderby = ["jobtitle"]; // Sort by Job Title
    var retrievedContacts = CrmServiceToolkit.queryByAttribute("contact", attributes, values, cols, orderby);
    
    alert(retrievedContacts[0].getValue('middlename'));
    NOTE: Again, the signature of this method has been changed in v2.1, please refer to the latest release page if you are using v2.1.

  10. Use CrmServiceToolkit.getCurrentUserId() to get the current user's ID.
    // Use CrmServiceToolkit.getCurrentUserId() to get the current user's ID.
    var currentUserId = CrmServiceToolkit.getCurrentUserId();
    
    alert(currentUserId);
  11. Use CrmServiceToolkit.getCurrentUserRoles() to get all the system roles that the current user has been assigned to.
    // Use CrmServiceToolkit.getCurrentUserRoles() to get all the system roles that the current user has been assigned to.
    var roles = CrmServiceToolkit.getCurrentUserRoles();
    
    alert(roles[0]); // Prompt the user's first role. 
  12. Use CrmServiceToolkit.isCurrentUserInRole() to check if the current user has a particular role.
    // Use CrmServiceToolkit.isCurrentUserInRole() to check if the current user has a particular role.
    var isSystemAdministrator = CrmServiceToolkit.isCurrentUserInRole("System Administrator");
    
    alert("I " + (isSystemAdministrator ? "AM" : "AM NOT") + " a System Administrator. "); 
As usual, here are a few notes about using the toolkit.
  1. The following CRM JavaScript functions have been used in order to keep the file size minimal (Aside from this reason, I am not a big fan of reinventing the wheel).
    • GenerateAuthenticationHeader() function
    • _HtmlEncode() function
    • CrmEncodeDecode.CrmXmlDecode() function
    • CrmEncodeDecode.CrmXmlEecode() function

    If you ever need to run the toolkit out of the context of a CRM form, you'll need to make the above functions available to the toolkit script.

  2. When you retrieve records from CRM using the toolkit's Fetch, Retrieve, RetrieveMultiple, or the new queryByAttribute methods, what you get will be the instance(s) of CrmServiceToolkit.BusinessEntity, which contains all CRM attributes (fields) that have been returned from CRM. However, you should not try to access those attributes directly, instead you use the instance function - getValue() or getValueAsBoolean() to get the value of the CRM field. The reason behind this is, CRM doesn't return anything if a field's value is null, in which case your JS code will blow up if you try to access the field (attribute) directly. With that being said, you should also be informed that a CRM filed's value could be null, be sure to handle it properly in your JS code.
  3. As mentioned previously, when dealing with the value of CRM bit data type that you have retrieved from CRM (Only Fetch, Retrieve, RetrieveMultiple, queryByAttribute methods are really concerned), you should use getValueAsBoolean() method to get the value. This seems to be the only field type that the toolkit cannot detect correctly. For all other type of CRM fields, you can pretty much use getValue() instance method to do the job.
  4. The toolkit will throw error if the CRM service calls failed with any exceptions, it's always a good idea to use try/catch block to manage the potential errors. An example would be:
    // It's always a good idea to contain any errors that could be thrown be the toolkit.
    try
    {
        var contactId = '3210F2BC-1630-EB11-8AB1-0003AAA0123C'; 
        var cols = ["firstname", "lastname", "familystatuscode", "creditlimit", "birthdate", "donotemail"];
        var retrievedContact = CrmServiceToolkit.Retrieve("contact", contactId, cols);
    
        // Do the rest of work
    }
    catch(err) {
        var errMsg = "There was an error when retrieving the contact information...\n\n";
        errMsg += "Error: " + err.description + "\n";
        alert(errMsg);
    }
  5. CRM's Execute message is a versatile message. Anything that you cannot easily achieve through the other 6 messages, you should resort to the toolkit’s Execute() method. Again, please refer to MSCRM 4.0 SDK for more CRM messages.
  6. The toolkit release has a test page included (CrmServiceToolkitTest.aspx), which utilizes QUnit as the test engine. In order to run the test script, you should deploy it along with all other files to ISV/CrmServiceToolkit folder (Please create this folder first), then you can launch http://crmserver:port/MyOrgName/ISV/CrmServiceToolkit/CrmServiceToolkitTest.aspx to run it. If you are in good luck, you should see a screen like this:
    CrmWebServiceToolkit2Test
    NOTE: The unit tests will actually write a contact record to your CRM database, and it will be deleted as part of the unit tests. 
I hope that I have covered everything.

[CREDITS] The idea behind CrmServiceToolkit.BusinessEntity was inspired by Ascentium CrmService JavaScript Library, after I have finished most of version 1.0 coding. Hats off to Ascentium CRM practice team.

P.S. You should probably have noticed that I have repeated most of the content in my previous toolkit release page, the reason is that I want to provide a single updated page for you to have all the information, so you don't have to go back and forth between the old release page and this release page.

Have fun with the toolkit, hope the toolkit can help you become a more productive CRM developper.

[UPDATE - July 4, 2010] A new version has been released at http://danielcai.blogspot.com/2010/07/crm-web-service-toolkit-for-javascript.html, please ensure to check out.

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!

Monday, May 03, 2010

MSCRM 4.0: Adding a Button to a Form Toolbar using Client Script

Usually we add custom button to CRM form through ISV.config customization, but there could be scenarios that you may want to add button on-fly in the form’s onLoad event. Here is the script that just does this job.
/**
 * Add a Button to a CRM4.0 form toolbar using client script.
 * @author Daniel Cai, http://danielcai.blogspot.com/
 */
function createToolbarButton(btnTitle, btnId, clickAction, imagePath, btnLabel, includeSpacer) {
    var toolbar = document.all.mnuBar1.rows(0).cells(0).childNodes[0];
    var html = (!includeSpacer) ? '' : '<li class="ms-crm-Menu-Spacer" tabIndex="-1">&nbsp;<img style="clip: rect(0px 4px 17px 0px); background-image: url(/_imgs/imagestrips/control_imgs_1.gif); width: 4px; background-position-y: -55px; height: 17px" alt="" src="/_imgs/imagestrips/transparent_spacer.gif"></li>';
    html += '<li id="' + btnId + '" + class="ms-crm-Menu" title="' + btnTitle + '" tabIndex="-1" onclick="window.execScript(action)" action="' + clickAction + '">';
    html += '<span class="ms-crm-Menu-Label"><a class="ms-crm-Menu-Label" tabIndex=-1 onclick="return false;" href="javascript:onclick();" target=_self>';
    html += (!imagePath) ? '' : '<img class="ms-crm-Menu-ButtonFirst" tabIndex="-1" alt="' + btnTitle + '" src="' + imagePath + '" />';
    html += '<span class="ms-crm-MenuItem-TextRTL" tabIndex=0>' + btnLabel + '</span></a></span>';
    
    toolbar.insertAdjacentHTML("beforeEnd", html);
}
To call the script, you may simply do this:
createToolbarButton("Test button", "mybuttonid", "myfunc()", "/_imgs/ico_16_4200.gif", "Test button label", true);
The code looks a little messy due to its lengthy html code, but once you have pasted to your file, it shouldn't look too bad. :-)

The parameters that the function accepts are all self-explanatory, hope you can figure out without requiring much explanation.

By the way, this is a re-post of my response to a question on CRM Development Forum.

Cheers!

Sunday, May 02, 2010

C#: A Simple Pseudo-Serializable Generic <string, string> Dictionary

Today I was trying to find an easy way to convert XML string to a C# generic <string, string> dictionary and the other way around. I wasn't able to find anything easy and simple enough, so I decided to write my own code to do the job.

I knew the first option would be XML Serializer, but there are a couple of issues that I don’t appreciate using it. First, I want to keep the xml file really simple, I don't like a full-blown XML file, as the more complex the XML file is, the easier people make mistakes when making changes to the XML file. Second, I want to be able to customize the dictionary root node's name, item node's name, and also key/value attribute's name, which doesn't seem to be viable using XML serializer. Lastly, C# generic dictionary is not serializable out-of-box, so it has to be custom serialization code like this one.

To be more specific, what I want is actually very simple, I want my code to convert the following XML string to a C# generic dictionary, and other way around the other time.
<settings>
  <setting key="setting1" value="value1" />
  <setting key="setting2" value="value2" />
</settings>
Here is the class that I implemented based on C# Dictionary class.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;

public class SimpleDictionary : Dictionary<string, string>
{
    /**
     * A Simple pseudo-serializable generic <string, string> dictionary
     * @author Daniel Cai, http://danielcai.blogspot.com/
     */
    private readonly string xsdMarkup = @"
<xs:schema id='dictionary' xmlns='' xmlns:xs='http://www.w3.org/2001/XMLSchema'>
  <xs:element name='{0}'>
    <xs:complexType>
      <xs:choice minOccurs='0' maxOccurs='unbounded'>
        <xs:element name='{1}'>
          <xs:complexType>
            <xs:attribute name='{2}' type='xs:string' />
            <xs:attribute name='{3}' type='xs:string' />
          </xs:complexType>
        </xs:element>
      </xs:choice>
    </xs:complexType>
  </xs:element>
</xs:schema>";

    private string _rootNodeName;
    private string _itemNodeName;
    private string _keyAttributeName;
    private string _valueAttributeName;

    public SimpleDictionary()
        : this("dictionary", "item", "key", "value")
    {

    }

    public SimpleDictionary(string rootNodeName, string itemNodeName, string keyAttributeName, string valueAttributeName)
    {
        _rootNodeName = rootNodeName;
        _itemNodeName = itemNodeName;
        _keyAttributeName = keyAttributeName;
        _valueAttributeName = valueAttributeName;
        xsdMarkup = string.Format(xsdMarkup, rootNodeName, itemNodeName, keyAttributeName, valueAttributeName);
    }

    public void FromXml(string xml)
    {
        Clear();

        XDocument xdoc = XDocument.Parse(xml);

        ValidateXml(xdoc);

        var dictionaryItemQuery = from element in xdoc.Root.Elements()
                       where
                           element.Name == _itemNodeName &&
                           element.Attributes().Count() == 2 &&
                           element.FirstAttribute.Name == _keyAttributeName &&
                           element.LastAttribute.Name == _valueAttributeName

                       select element;

        foreach (XElement keyValuePair in dictionaryItemQuery)
        {
            Add(keyValuePair.Attribute(_keyAttributeName).Value,
                keyValuePair.Attribute(_valueAttributeName).Value);
        }
    }

    public string ToXml()
    {
        XElement xElement = new XElement(_rootNodeName,
                                         from key in this.Keys
                                         select new XElement(_itemNodeName,
                                                             new XAttribute(_keyAttributeName, key),
                                                             new XAttribute(_valueAttributeName, this[key]))
            );
        return xElement.ToString();
    }

    private void ValidateXml(XDocument xdoc)
    {
        bool isValid = true;
        string errorMessage = string.Empty;

        XmlSchemaSet schemas = new XmlSchemaSet();
        schemas.Add("", XmlReader.Create(new StringReader(xsdMarkup)));

        xdoc.Validate(schemas, (sender, e) =>
        {
            errorMessage = string.Format("Validation error: {0}", e.Message);
            isValid = false;
        }, true);

        if (!isValid)
        {
            throw new XmlSchemaValidationException(errorMessage);
        }
    }
}
To convert a XML string into a generic <string, string> dictionary, you can write your code as below:
SimpleDictionary simpleDictionary = new SimpleDictionary("settings", "setting", "key", "value");
simpleDictionary.FromXml(@"
<settings>
  <setting key='key1' value='value1' />
  <setting key='key2' value='value2' />
</settings>");

// Your dictionary is now ready for use. 
To convert a <string, string> dictionary into XML string, the code should be something like this:
SimpleDictionary simpleDictionary = new SimpleDictionary();
simpleDictionary.Add("key1", "value1");
simpleDictionary.Add("key2", "value2");

string xml = simpleDictionary.ToXml();
Console.WriteLine(xml);

/* Output:
<dictionary>
  <item key='key1' value='value1' />
  <item key='key2' value='value2' />
</dictionary>"
*/
It's worth noting that the class has two constructors, the default one will use a default set of names (dictionary as the root node name, item as the dictionary item node name, key as the item's key attribute name, and value as the item's value attribute name). If you want the nodes and attributes to be called differently, you may call the other constructor by providing specific names.

Hope this helps.