Sunday, August 16, 2009

Using Unresolved Email Recipients in CRM 4.0

Unresolved emails are useful in that you can send an email to someone without having them setup as a system record (contact, account, lead, queue, user, ect).  Unresolved email addresses are turned off by default.  They should be used with caution.  If you send an email to an unresolved address it will not track the email to that lead/contact since one does not exist.  It also does not leverage the email "Do not allow" flags to allow people to opt out of emails.  With those considerations, there are still cases where it is very useful to send an email to someone without having to create a contact record.



I will show two example of sending emails to unresolved recipients.  The first will be sent from the email form using JavaScript.  The second will be done in a plugin.  We will add two unresolved email addresses as CC addresses on emails sent from a contact record.


Before we begin.  You must flip the setting to allow unresolved recipients.  Go to Settings --> Administration --> System Settings --> Email Tab.  Set Allow messages with unresolved e-mail recipients to be sent to yes.




 Second, we will add a couple of fields to the contact for secondary contacts email addresses.  These fields will contain our unresolved email addresses.




JavaScript Implementation
When the user clicks "Send Email" from the contact form we want the CC field to default with the two unresolved email addresses.  To do this we add the following Jscript to the onload of the email form.  The code will check to see if the regarding object is of type contact since this form is used for all emails.  Then it adds each email address to an email object that follows the activityparty schema for unresolved email addresses.  It has a type of 9206 and the email is set in the data property.

if (crmForm.FormType == 1 &&
    crmForm.all.regardingobjectid.DataValue != null &&
    crmForm.all.regardingobjectid.DataValue[0].typename == "contact" &&
    window.opener != null &&
    window.opener.document != null) {



    var ar = new Array();
    var emailObj = new Object();


    if (window.opener.document.crmForm.all.new_secondarycontactemail1 != null &&
            window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue != null) {
        emailObj = new Object();
        emailObj['type'] = '9206';
        emailObj['category'] = '3';
        emailObj['data'] = window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue;
        emailObj['name'] = window.opener.document.crmForm.all.new_secondarycontactemail1.DataValue;
        ar.push(emailObj);
    }



    if (window.opener.document.crmForm.all.new_secondarycontactemail2 != null &&
            window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue != null) {
        emailObj = new Object();
        emailObj['type'] = '9206';
        emailObj['category'] = '3';
        emailObj['data'] = window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue;
        emailObj['name'] = window.opener.document.crmForm.all.new_secondarycontactemail2.DataValue;
        ar.push(emailObj);
    }



    crmForm.all.cc.DataValue = ar;


}


Lastly, publish the customization.  When you click "Send Email" from the contact form your addresses will be added to the CC. 


 


 


Plugin Implementation


The same functionality can be added using a plugin.  The benefits of using a plugin versus a client side implementation is that you can implement consistent functionality regardless of whether the email send is triggered from a client portal, another plugin, or through the CRM UI.  The JavaScript implemenation is useful in that the user can see who the email is being sent to before they click Send. 



Install the plugin using the registration tool as a pre-create step for email save.  This will add the CC on the initial save of the email.  It is triggered regardless of whether the user clicks save or send from the email form.  The code also includes duplicate checking logic. So, if the user adds the email address to the CC from the form it will not duplicate the same address in the plugin.


using System.Web;
using System.Net;


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
using System.Reflection;
using System.Web.Services.Protocols;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Metadata;
using Inetium.CrmPlugins;
using System.Collections;



namespace Inetium.CrmPlugins.Plugins
{
    public class CaseUnresolvedEmailPlugin : IPlugin
    {
        public void Execute(IPluginExecutionContext context)
        {
            ICrmService service = null;
            try
            {
                if (context.InputParameters.Properties.Contains("Target") &&
                    context.InputParameters.Properties["Target"] is DynamicEntity)
                {
                    DynamicEntity entityInput = context.InputParameters.Properties["Target"] as DynamicEntity;
                    if (entityInput.Properties.Contains("regardingobjectid"))
                    {
                        Lookup regardingObjectLookup = entityInput.Properties["regardingobjectid"] as Lookup;
                        service = context.CreateCrmService(true);
                       
                        // Only using this for contacts
                        if (regardingObjectLookup.type == EntityName.contact.ToString())
                        {
                            // Pull parent contact - using helper method
                            DynamicEntity ctn = CRMUtilities.RetrieveById(service, EntityName.contact.ToString(), "contactid", regardingObjectLookup.Value, new ColumnSet(new string[]{"new_secondarycontactemail1", "new_secondarycontactemail2"}));
                            if (!ctn.Properties.Contains("new_secondarycontactemail1") && !ctn.Properties.Contains("new_secondarycontactemail2"))
                                return;



                            string email1 = ctn.Properties.Contains("new_secondarycontactemail1") ? ctn.Properties["new_secondarycontactemail1"].ToString() : "";
                            string email2 = ctn.Properties.Contains("new_secondarycontactemail2") ? ctn.Properties["new_secondarycontactemail2"].ToString() : "";


                            DynamicEntity[] partyArrayExisting = entityInput.Properties.Contains("cc") ? entityInput.Properties["cc"] as DynamicEntity[] : new DynamicEntity[] { };
                            List<DynamicEntity> partyList = new List<DynamicEntity>(partyArrayExisting);



                            if (email1 != "" && !IsEmailAlreadyInCC(email1, partyList))
                            {
                                DynamicEntity party = new DynamicEntity();
                                party.Name = EntityName.activityparty.ToString();
                                party.Properties["addressused"] = email1;
                                partyList.Add(party);
                            }



                            if (email2 != "" && !IsEmailAlreadyInCC(email2, partyList))
                            {
                                DynamicEntity party = new DynamicEntity();
                                party.Name = EntityName.activityparty.ToString();
                                party.Properties["addressused"] = email2;
                                partyList.Add(party);
                            }



                            // Generate CC's list by adding in those from contacts
                            if (partyList.Count > 0)
                                entityInput.Properties["cc"] = partyList.ToArray();


                        }


                    }
                }
            }
            catch (SoapException se)
            {
                throw new Exception(se.Detail.InnerText);
            }
            catch (Exception ex)
            {
                throw new Exception(ex.Message);
            }
            finally
            {
                if (service != null)
                {
                    service.Dispose();
                    service = null;
                }
            }
        }



        private bool IsEmailAlreadyInCC(string email, List<DynamicEntity> activityPartyList)
        {
            bool bAlreadyExists = false;
            foreach (DynamicEntity existingParty in activityPartyList)
            {
                // compare to existing CCs
                if (existingParty.Properties.Contains("addressused") &&
                    (existingParty.Properties["addressused"].ToString() == email))
                {
                    bAlreadyExists = true;
                    break;
                }
            }
            return bAlreadyExists;
        }
    }
}



Dynamics CRM 4.0 - JavaScript Web Service Helper Objects Part II

In February I posted a helper library for making JavaScript call to CRM - Original Post. Since then I have made a few enhancements.




  • Adding ordering to your results

  • Allowing for Null/Not Null Checks


The below example is used to default the address on a new contact to an address for their parent account. It pulls the first address (ordering by address number) where the address line 1 is not null (not null check).


try {
if (crmForm.all.parentaccountid.DataValue != null) {
// Create object passing in the entity you are selecting from
var crmService = new CrmService("customeraddress", LOGICAL_OPERATOR_AND);
crmService.AddColumn("name");
crmService.AddColumn("line1");
crmService.AddColumn("line2");
crmService.AddColumn("line3");
crmService.AddColumn("city");
crmService.AddColumn("stateorprovince");
crmService.AddColumn("postalcode");
crmService.AddColumn("country");

// Order by address number ascending
crmService.AddOrder("addressnumber", "Ascending");



// Add filter conditions (note: the "AND" logical operator was specified in constructor)
crmService.AddFilterCondition("parentid", crmForm.all.parentaccountid.DataValue[0].id, CONDITION_OPERATOR_EQUAL);

// Where line1 is not null
crmService.AddFilterCondition("line1", "", "NotNull");



// Retrieve the result object
var result = crmService.RetrieveMultiple();


// Loop through rows and select values (they return strings)
if (result.Rows.length > 0) {
var row = result.Rows[0];
// Get Column By Name
crmForm.all.address1_name.DataValue = row.GetValue("name");
crmForm.all.address1_line1.DataValue = row.GetValue("line1");
crmForm.all.address1_line2.DataValue = row.GetValue("line2");
crmForm.all.address1_line3.DataValue = row.GetValue("line3");
crmForm.all.address1_city.DataValue = row.GetValue("city");
crmForm.all.address1_stateorprovince.DataValue = row.GetValue("stateorprovince");
crmForm.all.address1_postalcode.DataValue = row.GetValue("postalcode");
crmForm.all.address1_country.DataValue = row.GetValue("country");



}
}
}
catch (e) {
alert(e.message);


}


The new library is attached below. Enjoy!


-Andrew



This release is provided "AS IS" and contains no warranty and confers no rights.



ASP.Net Caching with Dynamics CRM Data

When writing websites that work with CRM data you have a couple options when it come to binding PickList attributes.  You can hard-code the options into list items.


        Account Relationship Type:
        <br />
        <asp:DropDownList runat="server" ID="accountRelationshipType">
            <asp:ListItem Value="1">Competitor</asp:ListItem>
            <asp:ListItem Value="2">Consultant</asp:ListItem>
            <asp:ListItem Value="3">Customer</asp:ListItem>
            <asp:ListItem Value="4">Investor</asp:ListItem>
            <asp:ListItem Value="5">Partner</asp:ListItem>
            <asp:ListItem Value="6">Influencer</asp:ListItem>
            <asp:ListItem Value="7">Press</asp:ListItem>
            <asp:ListItem Value="8">Prospect</asp:ListItem>
            <asp:ListItem Value="9">Reseller</asp:ListItem>
            <asp:ListItem Value="10">Supplier</asp:ListItem>
            <asp:ListItem Value="11">Vendor</asp:ListItem>
            <asp:ListItem Value="12">Other</asp:ListItem>
        </asp:DropDownList>



This option is tedious and it requires a code change if a system customizer adds another pick list value.  The second option is to hit the metadata service to retrieve the available options and then bind them to the dropdown list.  To pull back the metadata every time, performance is a concern, especially when you have multiple dropdown lists.  It is better to leverage ASP.Net caching to store the data.  You can set up a cache dependency to re-pull the data every X number of hours.


To get started you need to add a web reference to the MetaData web service.



The URL is http://andrewvpc:5555/mscrmservices/2007/metadataservice.asmx where andrewvpc is your servername and 5555 is your port number.  This does not require the uniquename as is necessary with the CRM service.



 


Setup MetaData Service Connection


Next, setup the connection by adding the following code.  The code is similar to connecting to the CRM service with a couple differences.


        private static String _org = System.Configuration.ConfigurationManager.AppSettings["CRMWSOrg"];
        private static String _userName = System.Configuration.ConfigurationManager.AppSettings["CRMWSUser"];
        private static String _password = System.Configuration.ConfigurationManager.AppSettings["CRMWSPassword"];
        private static String _domain = System.Configuration.ConfigurationManager.AppSettings["CRMWSDomain"];
        private static String CRM_WSURL = System.Configuration.ConfigurationManager.AppSettings["CRMWS"];
        private static String CRM_SERVER = System.Configuration.ConfigurationManager.AppSettings["CRMSERVER"];



        public static MetadataService.MetadataService InitiateMetaDataService(bool useSuperAdmin)
        {


            MetadataService.MetadataService service = new MetadataService.MetadataService();
            try
            {

                service.Url = CRM_SERVER + "metadataservice.asmx";

                if (useSuperAdmin)
                    service.Credentials = new NetworkCredential(_userName, _password, _domain);
                else
                    service.Credentials = System.Net.CredentialCache.DefaultCredentials;




                MetadataService.CrmAuthenticationToken token = new MetadataService.CrmAuthenticationToken();
                token.AuthenticationType = 0;
                token.OrganizationName = (string)System.Configuration.ConfigurationManager.AppSettings["CRMWSOrg"];


                service.CrmAuthenticationTokenValue = token;



                return service;
            }
            catch (Exception ex)
            {
                throw new Exception("Exception caught in InitiateMetaDataService: " + ex.Message);
            }
        }



 


Get PickList Data from MetaData Service


After connecting to the service, create a RetrieveAttributeRequest for a given entity and attribute.


        public static Option[] GetPickListValues(string entity, string attribute)
        {
            try
            {
                using (MetadataService.MetadataService service = CachingHelper.InitiateMetaDataService(true))
                {
                    RetrieveAttributeRequest req = new RetrieveAttributeRequest();
                    req.EntityLogicalName = entity;
                    req.LogicalName = attribute;
                    RetrieveAttributeResponse resp = service.Execute(req) as RetrieveAttributeResponse;
                    if (resp.AttributeMetadata as PicklistAttributeMetadata == null)
                        throw new ApplicationException("Not picklist attribute");



                    PicklistAttributeMetadata attributeMetaData = resp.AttributeMetadata as PicklistAttributeMetadata;
                    return attributeMetaData.Options;
                }
            }
            catch (SoapException ex2)
            {
                throw new Exception("Exception caught in GetPickListValues:  " + ex2.Detail.InnerText, ex2);



            }
            catch (Exception ex)
            {
                throw new Exception("Exception caught in GetPickListValues:  " + ex.Message);
            }
        }
 



Load Data from Cache


Use ASP.Net caching to store the data in the cache if it is expired.  Set a cache dependency of for X number of hours.  Retrieve the data from the cache and bind it to the drop down list.  The label is a little tricky because the label contains text for each culture.  The value is the int value associated with the PickList.

        public void LoadFromCache(string entityName, string attributeName, DropDownList list, int hoursToCache, Page page)
        {
            // If cache expired then reset in cache
            if (page.Cache[attributeName] == null)
            {
                // Get from CRM
                Option[] options = App_Code.CachingHelper.GetPickListValues(entityName, attributeName);
                page.Cache.Insert(attributeName, options, null, DateTime.Now.AddHours(hoursToCache), TimeSpan.Zero);
            }



            // Pull from cache
            Option[] optionsFromCache = page.Cache[attributeName] as Option[];
            list.Items.Clear();
            foreach (Option option in optionsFromCache)
            {
                string label = "";
                // Grab first label or add extra logic to grab the label for the
                // right culture
                if (option.Label.LocLabels.Length > 0)
                    label = option.Label.LocLabels[0].Label;



                list.Items.Add(new ListItem(label, option.Value.Value.ToString()));
            }
        }
       
    }


Create a Web Page


Create a web page and add a DropDownList .


Account Relationship Type:
<br />
<asp:DropDownList runat="server" ID="accountRelationshipType">
</asp:DropDownList>



 


Call Cached Data from Page


On page load bind the data to the DropDownList.


protected void Page_Load(object sender, EventArgs e)
        {
            if (!this.IsPostBack)
            {
                App_Code.CachingHelper helper = new CacheDemo.App_Code.CachingHelper();
                helper.LoadFromCache("account", "customertypecode", accountRelationshipType, 5, this);
            }
        }



 


Hit Run... ...  The data will be pulled from the CRM the first time and from the cache on subsequent calls.