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.

Wednesday, April 21, 2010

MSCRM 4.0: Hide Duplicate Titles of CRM Notes Field

There was a question on CRM Development Forum about how to hide one of the titles of CRM notes field. The requirement is to make it looks like something as below.

Before hiding:

HideNotesTitle1

After hiding the titles:

HideNotesTitle

Here is the script that I came up.

(function hideNotesTitle() {
    var notesField = crmForm.all.notescontrol;
    if (!notesField) return;  // there is no notes field on current form

    var onReadyStateChange = function() {
        if (notesField.readyState === 'complete') {
            var notesDoc = notesField.contentWindow.document;
            var notesTable = notesDoc.getElementById("NotesTable");
            var notesTableBody = notesTable.childNodes[1];

            for (var i = 0, len = notesTableBody.childNodes.length; i < len; i++) {
                var row = notesTableBody.childNodes[i];
                if (row.className == "noteHeader" && !row.oId) {
                    row.style.display = "none";
                }
            }

            notesField.detachEvent('onreadystatechange', onReadyStateChange);
        }
    };

    notesField.attachEvent('onreadystatechange', onReadyStateChange);
})();

To use the function, you simply copy the code to your CRM form's OnLoad event.

Hope this snippet gives you some idea about how to work with CRM notes field if you ever need to do something similar.

Cheers!