Sunday, May 31, 2009

CRM 4.0 Embedding User Signature in CRM Web Client

One of the main features that the CRM email web client is missing is an automatic stationary signature. Although a user (or an administrator) can create a predefined signature and embed it before sending the email the web client requires the user to fill the TO (or CC) field in advance and then pick out the signature template using the Insert Template button. For most of us who gotten used to using advanced editors like outlook this seems like a major step backwards.

In order to enhance the user experience and demonstrate to strength and simplicity of dynamics while I’m at it I wrote a post that automates the process. The automated signature makes use of CRM’s email template feature. Once the Global email template (signature) is in place you need to extract its id (templateid) and use it in your code.

In order to retrieve the template id you can run this following query against the filteredtemplate view:

SELECT TEMPLATEID FROM FILTEREDTEMPLATE WHERE TITLE = ‘USER SIGNATURE’


The nice thing about dynamics is that most UI features also have an API manifestation. In this case it’s the ability to instantiate email templates by using the InstantiateTemplateRequest. The InstantiateTemplateRequest receives a templateid, objectid and objecttype. The objectid and type are used as context so the template is able to retrieve information that is specific to the recipient entity. Since we are only interested in the user information the objectid and type are filled with the email owner which is the current user.

Copy the following code to the email onload event and enjoy…


function Signature(companyTemplateId)
{
var sig = this;
var emailIframe;
var emailBody;

sig.TemplateId = companyTemplateId;

sig.Load = function()
{
try
{
var xml = '' +
'' +
'' +
GenerateAuthenticationHeader() +
' ' +
' ' +
' ' +
' ' + sig.TemplateId + '' +
' ' + crmForm.ownerid.DataValue[0].typename + '' +
' ' + crmForm.ownerid.DataValue[0].id + '' +
'
' +
'
' +
'
' +
'
' +
'';

var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
xmlHttpRequest.open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest.setRequestHeader("SOAPAction","http://schemas.microsoft.com/crm/2007/WebServices/Execute");
xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest.send(xml);

var resultXml = xmlHttpRequest.responseXML;
if (xmlHttpRequest.status == 200)
{
emailBody = resultXml.selectSingleNode("//q1:description").text;
emailIframeReady();
}
}
catch(err)
{
alert(err.description);
}
}

function emailIframeReady()
{
if (emailIframe.readyState != 'complete')
{
return;
}

emailIframe.contentWindow.document.body.innerHTML = emailBody;
}

emailIframe = document.all.descriptionIFrame;
emailIframe.onreadystatechange = emailIframeReady;
}

function OnCrmPageLoad()
{
if (crmForm.FormType == 1)
{
var signature = new Signature("90886EF8-1A4D-DE11-9CF8-0003FF230264");
signature.Load();
}
}

OnCrmPageLoad();

Saturday, May 30, 2009

CRM 4.0 Finding Entity URL

This is a small trick you can use to find what URL is stored behind each CRM vanilla (customizable) entity. You can use as part of your code (see my post about cloning an entity using JavaScript) or just run it in IE address bar. Although it seems this function is not going anywhere there is a chance it won’t be there on the next version.

Address bar function:
javascript:void( alert( getObjUrl( 2 ) ) )


The getObjUrl function utilizes the GetWindowInformation function which receives the entity object type code e.g. 2 – contact and return an object containing the entity Url and window size (width and height). Here is an example of how to use it inside your code:

var contactUrlInfo = GetWindowInformation(2);
var windowFeatures = “height=” + contactUrlInfo.Height + “,width=” + contactUrlInfo.Width + “,toolbars=0”;
window.open( prependOrgName( contactUrlInfo.Url ) , “contact window” , windowFeatures );


So how does this work?
The GetWindowInformation function refers to “/_common/windowinformation/windowinformation.aspx” URL. If you open this page in the address bar and take a look at the page source you’ll see a script that look like this:


function CRMWindowInfo(sUrl, iXOffset, iYOffset)
{
this.Width = parseInt(iXOffset, 10);
this.Height = parseInt(iYOffset, 10);
this.Url = sUrl;
}
function GetWindowInformation(iObjectType) {
switch (parseInt(iObjectType, 10))
{
case Account: return new CRMWindowInfo("sfa/accts/edit.aspx",1000,560);
case List: return new CRMWindowInfo("ma/lists/edit.aspx",820,560);
//and so on…
}
}


In order not to rely on ms functionality you can either work with static values or create your own page under the isv folder the returns similar js.

Friday, May 29, 2009

CRM 4.0 Show Associated-View in IFRAME (AssocViewer)

This post is complementary to the other posts that discuss presenting a CRM gird inside an IFRAME. As usually when you display an associated view inside an IFRME some of MS functionality breaks e.g. the automatic refresh when you add an existing member. This post uses the same technique as the N2NViewer but overrides the ms _locAssocOneToMany function in order to know when to refresh the grid.

The object exposes the following properties:
1. ParentId – this is the parent entity key. If not specified the viewer uses the current entity crmForm.ObjectId.
2. ParentOtc – this is the parent object type code. Again if not specified the crmForm.ObjectTypeCode is used.
3. RelationshipName – this is the relationship id as it appears in customization.
4. Tabset – this is the name of the tabset parameter which is required for the associated view to work. Use the iedevtoolbar to extract this parameter.

Enjoy...


function OnCrmPageLoad()
{
var assocViewer = new AssocViewer("IFRAME_RelatedContacts");
/* Optional - crmForm.ObjectId */
assocViewer.ParentId = 'D74D44AD-36D0-DC11-AA32-0003FF33509E';
/* Optional - crmForm.ObjectTypeCode */
assocViewer.ParentOtc = 1;
/* Mandatory - relationship schema name */
assocViewer.RelationshipName = "contact_customer_accounts"
/* Mandatory - tabset query string parameter */
assocViewer.Tabset = "areaContacts";
assocViewer.Load();
}

function AssocViewer(iframeId)
{
var av = this;
if (crmForm.FormType == 1)
{
return;
}

av.IFrame = document.all[iframeId];
if (!av.IFrame)
{
alert("Iframe " + iframeId + " is missing!");
}

var _locAssocOneToMany = null;
av.ParentId = crmForm.ObjectId;
av.ParentOtc = crmForm.ObjectTypeCode;
av.Tabset = null;
av.RelationshipName = null;

av.Load = function()
{
if (av.ParentId == null || av.ParentOtc == null || av.Tabset == null || av.RelationshipName == null)
{
return alert("Missing Parameters: ParentId or ParentOtc or Tabset Name!");
}
var security = crmFormSubmit.crmFormSubmitSecurity.value;
av.IFrame.src = "areas.aspx?oId=" + av.ParentId + "&oType=" + av.ParentOtc + "&security=" + security + "&tabSet=" + av.Tabset
av.IFrame.onreadystatechange = av.OnIframeReady;
}

av.OnIframeReady = function()
{
if (av.IFrame.readyState != 'complete')
{
return;
}

av.IFrame.contentWindow.document.body.scroll = "no";
av.IFrame.contentWindow.document.body.childNodes[0].rows[0].cells[0].style.padding = "0px";

_locAssocOneToMany = locAssocOneToMany;
locAssocOneToMany = av.locAssocOneToMany;
}

av.locAssocOneToMany = function(iType, sRelationshipName)
{
_locAssocOneToMany(iType,sRelationshipName);
if (sRelationshipName == av.RelationshipName)
{
av.IFrame.contentWindow.document.all.crmGrid.Refresh();
}
}
}

//Entry Point
OnCrmPageLoad();

Tuesday, May 26, 2009

CRM 4.0 Multi Lingual Support in Plug-ins

The following code presents a simple yet very effective way to handle and return multi-lingual error messages form a plug-in.

So how does it work?

Basically I created a base class for Custom Exceptions. All custom exceptions that you use in your code derive from this class. The base class overrides the Exception class Message property and returns the error message from a resources dictionary.

The resource dictionary holds the translations for each local id e.g. (1033, 3082). When an exception is thrown the user local id (LCID) is taken from the current running thread Culture Info object.

The Inner translation dictionary manages the each error message depending on the exception type. This way when the code throws a specific exception the correct error message is fetched from the dictionary.

Enjoy…


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
using System.Web.Services.Protocols;

namespace Empty.PlugIn
{
public class Handler : IPlugin
{
#region IPlugin Members
/// <summary>
/// Resource Dictionary
/// </summary>
public static Dictionary<Int32, Dictionary<Type, String>> Resources;
public void Execute(IPluginExecutionContext context)
{
if (Resources == null)
{
Resources = new Dictionary<Int32, Dictionary<Type, String>>();

Dictionary<Type, String> English = new Dictionary<Type, String>();
English.Add(typeof(Exception), "General Exception");
English.Add(typeof(SoapException), "CRM Exception");
English.Add(typeof(InvalidCustomException), "Invalid Custom Exception");
English.Add(typeof(UnKnownPluginException), "UnKnown Plugin Exception");
Resources.Add(1033, English);

Dictionary<Type, String> Spanish = new Dictionary<Type, String>();
Spanish.Add(typeof(Exception), "Excepciףn general");
Spanish.Add(typeof(SoapException), "Excepciףn de CRM");
Spanish.Add(typeof(InvalidCustomException), "Excepciףn personalizado no vבlido");
Spanish.Add(typeof(UnKnownPluginException), "Excepciףn Desconocida Plugin");
Resources.Add(3082, Spanish);
}

try
{
//throw new Exception();
//throw new SoapException();
throw new UnKnownPluginException();
}
catch(Exception ex)
{
throw new InvalidPluginExecutionException(ex.Message);
}
}

public abstract class CustomException : Exception
{
private Int32 DefaultLanguage = 1033;
/// <summary>
/// override default message prop
/// </summary>
public override string Message
{
get
{
return this.GetResource(this.GetType());
}
}
/// <summary>
/// Get the resource by exception type
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
protected string GetResource(Type type)
{
Int32 uiLcid = System.Threading.Thread.CurrentThread.CurrentUICulture.LCID;

if (Resources.ContainsKey(uiLcid))
{
return Resources[uiLcid][type];
}

return Resources[this.DefaultLanguage][type];
}
}

public class InvalidCustomException : CustomException
{
}

public class UnKnownPluginException : CustomException
{
}

#endregion
}
}

Sunday, May 3, 2009

CRM 4.0 Using attribute mapping in code


This is a quick and dirty way of getting CRM attribute mapping. So what exactly can you do with it? One option would be to use it in order to retrieve parent context information when you use CrmService to create new entities. For example, when you create a new contact you might want to get the account information that would normally map to contact when used from the CRM UI.

One of the challenges of using SDK is to mimic some of the functionalities that exist in the UI e.g. required field and attribute mapping when a child entity is created in the context of a parent entity. This type of mapping does not exist out of the box and requires you to write custom code.

The following example is a generic way to extract mapping information from CRM. So what are the pros and cons of using this technique?
The biggest advantage is that you’re able to you CRM customizations which make the configuration process worth while.
The disadvantages are that the default CRM mapping (attributemap entity) does not hold the type of the attributes e.g picklist or lookup and the information returned form the CRM also contains inner mappings of picklist and lookups e.g. parentcustomeridname and parentcustomriddsc.

So how can we overcome these disadvantages?
The attribute type can easily be retrieved using the metadata service RetrieveAttributeRequest. The problem is that this call to metadataservice is very slow so you also need to cache the data in order to use it efficiently.
The inner mapping can be ignored if you exclude picklist and lookup attributes that end with name of dsc.

The code does not cache the mapping information and does not show you how to create a new contact with parent mapping but presents a poc of how to retrieve the mapping information for further use.

The following is what the program prints to the console:

paymenttermscode --> paymenttermscode - Type: Picklist
telephone1 --> telephone1 - Type: String
accountid --> parentcustomerid - Type: Customer
name --> parentcustomeridname - Type: String
deletionstatecode --> parentcustomeriddsc - Type: Integer
address1_addresstypecode --> address1_addresstypecode - Type: Picklist
address1_city --> address1_city - Type: String
address1_country --> address1_country - Type: String
address1_county --> address1_county - Type: String
address1_line1 --> address1_line1 - Type: String

Here is the code, simply create a new console application and dump the class inside the program.cs file


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;
using Microsoft.Crm.SdkTypeProxy.Metadata;
using Microsoft.Crm.Sdk.Metadata;

namespace EntityMapping
{
class Program
{
static void Main(string[] args)
{
List<AttributeMapping> AcctContMap = GetAttributeMapping("account", "contact");

foreach(AttributeMapping attributeMap in AcctContMap)
{
Console.WriteLine
(
String.Format
(
"{0} --> {1} - Type: {2}",
attributeMap.FromEntityName,
attributeMap.ToEntityName,
Enum.GetName(typeof(AttributeType),attributeMap.AttributeType)
)
);
}
}

public class AttributeMapping
{
public String FromEntityName;
public String ToEntityName;
public AttributeType AttributeType;

public AttributeMapping(String fromEntityName,
String toEntityName,
AttributeType typeofAttribute)
{
this.FromEntityName = fromEntityName;
this.ToEntityName = toEntityName;
this.AttributeType = typeofAttribute;
}
}

private static List<AttributeMapping> GetAttributeMapping(String fromEntity, String toEntity)
{
DynamicEntity entityMap = GetEntityMapping(fromEntity, toEntity);
List<BusinessEntity> startMap = RetieveAttributeMappingByEntityMap(entityMap);
List<AttributeMapping> endMap = new List<AttributeMapping>(startMap.Count);

foreach (DynamicEntity attributeMap in startMap)
{
String targetAttribute = attributeMap.Properties["targetattributename"].ToString();
String sourceAttribute = attributeMap.Properties["sourceattributename"].ToString();
endMap.Add(
new AttributeMapping(sourceAttribute,targetAttribute,
GetAttributeType(toEntity,targetAttribute))
);
}

return endMap;
}

private static List<BusinessEntity> RetieveAttributeMappingByEntityMap(DynamicEntity entityMap)
{
Guid entityMapId = ((Key)entityMap.Properties["entitymapid"]).Value;
QueryExpression attributeQuery = GetAttributeQuery(entityMapId, new AllColumns());
return RetrieveMultiple(attributeQuery);
}

private static AttributeType GetAttributeType(string entityLogicalName, string attributeLogicalName)
{
RetrieveAttributeRequest attributeRequest = new RetrieveAttributeRequest();
attributeRequest.EntityLogicalName = entityLogicalName;
attributeRequest.LogicalName = attributeLogicalName;
RetrieveAttributeResponse attributeResponse =
(RetrieveAttributeResponse)GetMetaService("http://moss:5555/","MicrosoftCRM").Execute(attributeRequest);
return attributeResponse.AttributeMetadata.AttributeType.Value;
}

private static MetadataService GetMetaService(string serverUrl, string orgName)
{
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.AuthenticationType = 0;
token.OrganizationName = orgName;

MetadataService service = new MetadataService();
service.Url = String.Format("{0}mscrmservices/2007/metadataservice.asmx", serverUrl);
service.PreAuthenticate = false;
service.UnsafeAuthenticatedConnectionSharing = true;
service.CrmAuthenticationTokenValue = token;
service.UseDefaultCredentials = true;

return service;
}

private static List<BusinessEntity> RetrieveMultiple(QueryExpression query)
{
RetrieveMultipleRequest request = new RetrieveMultipleRequest();
request.Query = query;
request.ReturnDynamicEntities = true;
RetrieveMultipleResponse response =
(RetrieveMultipleResponse)GetCrmService(
"http://moss:5555/","MicrosoftCRM").Execute(request);
return response.BusinessEntityCollection.BusinessEntities;
}

private static CrmService GetCrmService(string serverUrl, string orgName)
{
CrmAuthenticationToken token = new CrmAuthenticationToken();
token.AuthenticationType = 0;
token.OrganizationName = orgName;

CrmService service = new CrmService();
service.Url = String.Format("{0}mscrmservices/2007/crmservice.asmx",serverUrl);
service.PreAuthenticate = false;
service.UnsafeAuthenticatedConnectionSharing = true;
service.CrmAuthenticationTokenValue = token;
service.UseDefaultCredentials = true;

return service;
}

private static DynamicEntity GetEntityMapping(String fromEntity, String toEntity)
{
QueryExpression query = new QueryExpression(EntityName.entitymap.ToString());
query.ColumnSet.AddColumn("entitymapid");
query.Criteria.AddCondition(
new ConditionExpression("sourceentityname",ConditionOperator.Equal,
new object[]{fromEntity})
);
query.Criteria.AddCondition(
new ConditionExpression("targetentityname",ConditionOperator.Equal,
new object[]{toEntity})
);

List<BusinessEntity> entityMapping = RetrieveMultiple(query);
return entityMapping[0] as DynamicEntity;
}

private static QueryExpression GetAttributeQuery(Guid entityMapId, ColumnSetBase columnSet)
{
QueryExpression query = new QueryExpression(EntityName.attributemap.ToString());
query.ColumnSet = columnSet;
query.Criteria.AddCondition(
new ConditionExpression("entitymapid",ConditionOperator.Equal,entityMapId.ToString())
);
return query;
}
}
}