Saturday, February 14, 2009

CRM 4.0 Concatenating Fields Plug-In


A few weeks back I carried out a boot-camp training course to a team of dot net developers. I have to say it’s quite amusing to see the frustration on experienced developers faces when they realize the amount of energy one needs to invest in order to implement a requirement as simple as the one covered in this post. No doubt dynamics requires getting use to, but the benefits of using such a vast platform makes it all worthwhile.

Many CRM implementations require you to construct a field’s value by concatenating other fields. There are hands full of reasons for implementing this type of functionality and Most have to do with ways data is displayed or what I call data usability.

There are three common approaches which one can utilize. If you find yourself in that crossroad consider the pros and cons of each approach before you start developing.


The first solution is attaching a JavaScript event handler to the CRM form OnSave event. This allows you to set the target field before the form is saved and data is actually sent to the server. There is no arguing about the easiness of this approach since it only requires a few lines to get this done.

Here is an example which you can follow:




function OnCrmPageLoad()
{
crmForm.attachEvent(“onsave”,OnCrmPageSave);
}

function OnCrmPageSave()
{
var attribute1Value = (crmForm.new_attribute1.DataValue)? crmForm.new_attribute1.DataValue:””;
var attribute2Value = (crmForm.new_attribute2.DataValue)? crmForm.new_attribute2.DataValue:””;
crmForm.new_targetattribute.DataValue = attribute1Value + “ ” + attribute2Value;
}



This method has obvious drawbacks which must be considered. One which is especially important is when the application needs to utilize clients other then Internet explorer such as excel when importing data or a web service when constructing data from code. Of course this is more of a general statement since writing client side code is never about a client but about all available ones. This is something many dynamics developers tend to oversee.


The second approach is creating a workflow using the workflow designer. Although this provides a remedy to the dilemma presented above, the method is not bullet proof and has its own drawbacks. The first one has to do with the way the workflows operate. Since the workflow process is asynchronous the user won’t see the result concatenation until he reopens the record. A second disadvantage worth mentioning has to do with designer limitations. Consider a scenario where you need to concatenate the values of a lookup field and a text field. The workflow designer does not allow you to concatenate dynamic values which are not of the same type. This might convince you choose the third and final approach which is using a plug-in

Although this might take longer to implement, planning and coding it once will no doubt save you plenty of time down the road. And since the following plug-in offers a generic solution you’ll get there even faster. The plug-in also offers a remedy to the workflow drawbacks mentioned above.

Basically, in order to provide full coverage, the plug-in needs to run on both post create and post update events and also require post event images from which the values are read and concatenated. This approach seems unnecessary at first but since lookup and customer types don’t carry their names (just guids) to the plug-in the only way to retrieve the actual names is to use an image.

In order to make the process friendlier I added a configuration xml which enables you to configure multiple target fields for each entity and set the desired format for each target field.


Bellow is a sample configuration xml. You must add it to the post-create and post-update unsecure configuration boxes. You might wonder why this is required for both events. The reason is that the plug-in needs to address both the creation of a new record and the update of existing records when it loads / cached and re-cached by CRM.


This sample uses fields from the incident entity as a showcase. This is also the case for the plug-in solution xml file which is displayed at the bottom of the post.




<Entity>
<Target ID="title" Format="{0} - {1}">
<Field ID="subjectid" Type="Lookup"/>
<Field ID="customerid" Type="Customer"/>
</Target>
</Entity>


The Target node describes the field whose value is constructed from the inner Field nodes. The Target ID attribute must specify and existing attribute name. The plug-in code makes use of the String.Format method which receives a format string such as “{0} – {1}” and a list of optional parameters. Each Field Node must specify an existing entity attribute and correct Type. The Type attribute is obviously used to differentiate between CRM types.


The Generic concatenation Plug-in is build on top of 3 classes. A Handler class which implements the IPlugIn Interface. A TargetField class which represent a single Target. And the ConcatPlugin proxy class which implements the concatenation process. I’ve also added remarks and regions to make the code more readable.



using System;
using System.Xml;
using System.Collections.Generic;
using System.Text;

using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;

namespace GenericConcatPlugIn
{
///
/// Plug-In Handler
///

public class Handler : IPlugin
{
#region IPlugin Members
///
/// Holds all fields that require concatenation
///

private Dictionary Targets;
///
/// plug-in constructor
///

///
///
public Handler(String config, String secureConfig)
{
XmlDocument TargetsDocument = TryLoadTargetsXml(config);

#region Build Targets Settings

XmlNodeList TargetsNodeList = TargetsDocument.SelectNodes("//Target");
this.Targets = new Dictionary(TargetsNodeList.Count);

foreach (XmlElement ndField in TargetsNodeList)
{
TargetField Target = new TargetField(ndField);
if (this.Targets.ContainsKey(Target.ID))
{
this.Targets[Target.ID] = Target;
}
else
{
this.Targets.Add(Target.ID, Target);
}

}

#endregion
}
///
/// Attempt to load entity configuration xml
///

///
///
private XmlDocument TryLoadTargetsXml(String configXml)
{
#region Validate Plugin Configuration

if (configXml == null || configXml.Length == 0)
{
throw new InvalidPluginExecutionException("Initialize: Configuration Xml cannot be null");
}

#endregion

#region Return Configuratoin Document

try
{
XmlDocument document = new XmlDocument();
document.LoadXml(configXml);
return document;
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException("Initialize: Error loading Configuration Xml Document", ex);
}

#endregion
}
///
/// Plug-in Execute implementation
///

///
public void Execute(IPluginExecutionContext context)
{

#region Execute Concatenation Plugin

ConcatPlugin contactPlugIn = new ConcatPlugin();
contactPlugIn.Context = context;
contactPlugIn.Execute(Targets);

#endregion
}

#endregion
}
///
/// A Field whose value is constructed (concatenated) from other fields
///

public class TargetField
{
#region Target Field Members

public String ID = String.Empty;
public String Format = String.Empty;
public Dictionary Related;

#endregion

public TargetField(XmlElement ndTarget)
{
#region Set Field Members

this.Related = new Dictionary();
this.ID = ndTarget.Attributes["ID"].Value;
this.Format = ndTarget.Attributes["Format"].Value;

#endregion

#region Set Field Related (Concatenated) Fields

XmlNodeList RelatedFieldList = ndTarget.SelectNodes("Field");

foreach (XmlElement related in RelatedFieldList)
{
String relatedId = related.Attributes["ID"].Value;
String relatedType = related.Attributes["Type"].Value;

if (!Related.ContainsKey(relatedId))
{
Related.Add(relatedId, relatedType);
}
}

#endregion

#region Sample Target Xml Configuration
/*





*/
#endregion
}
}
///
/// Plug-in proxy
///

public class ConcatPlugin
{
///
/// Plug-in Original Context
///

public IPluginExecutionContext Context;
///
/// Plug-in Main method
///

///
internal void Execute(Dictionary Targets)
{
#region Validate Target Entity

if (!this.Context.InputParameters.Contains(ParameterName.Target))
{
return;
}

#endregion

#region Concatenate Target Fields

DynamicEntity Current = null;
foreach (KeyValuePair Target in Targets)
{
Current = ((DynamicEntity)this.Context.InputParameters[ParameterName.Target]);
Current.Properties.Add(ConcatProperty(Target.Value));
}

if (Current == null)
{
return;
}

#region Update the target

if (this.Context.MessageName == "Create")
{
if( !this.Context.OutputParameters.Contains("id") )
{
return;
}

ICrmService Service = this.Context.CreateCrmService(true);
Key entityId = new Key( new Guid( this.Context.OutputParameters["id"].ToString() ) );
KeyProperty entityKey = new KeyProperty(this.Context.PrimaryEntityName + "id",entityId);
Current.Properties.Add(entityKey);
Service.Update(Current);
}
else if (this.Context.MessageName == "Update" && this.Context.Depth == 1)
{
ICrmService Service = this.Context.CreateCrmService(true);
Service.Update(Current);
}
#endregion

#endregion
}
///
/// Reference to Target Dynamic Entity being created or updated
///

private DynamicEntity Target
{
get { return this.Context.InputParameters[ParameterName.Target] as DynamicEntity; }
}
///
/// Reference to the Target Property Collection
///

private PropertyCollection PostImageProperties
{
get
{
return ((DynamicEntity)this.Context.PostEntityImages[ParameterName.Target]).Properties;
}
}
///
/// Concatenates the target fields and returns the a StringProperty
///

///
///
private StringProperty ConcatProperty(TargetField Target)
{
List TargetValues = new List();
StringProperty TargetProperty = new StringProperty();
TargetProperty.Name = Target.ID;

#region Retrieve Each Concatenated Field Value

foreach (KeyValuePair related in Target.Related)
{
String fieldId = related.Key;
String fieldType = related.Value;

#region Get Field Value or Name by Type

PropertyCollection Properties = PostImageProperties;

switch (fieldType)
{
case "String":
TargetValues.Add(Properties[fieldId] + "");
break;
case "CrmNumber":
TargetValues.Add(((CrmNumber)Properties[fieldId]).Value + "");
break;
case "CrmFloat":
TargetValues.Add(((CrmFloat)Properties[fieldId]).Value + "");
break;
case "CrmDecimal":
TargetValues.Add(((CrmDecimal)Properties[fieldId]).Value + "");
break;
case "CrmMoney":
TargetValues.Add(((CrmMoney)Properties[fieldId]).Value + "");
break;
case "Lookup":
TargetValues.Add(((Lookup)Properties[fieldId]).name + "");
break;
case "Owner":
TargetValues.Add(((Owner)Properties[fieldId]).name + "");
break;
case "Customer":
TargetValues.Add(((Customer)Properties[fieldId]).name + "");
break;
}
#endregion
}

#endregion

TargetProperty.Value = String.Format(Target.Format, TargetValues.ToArray());
return TargetProperty;
}
}
}



Following is the plug-in solutionxml. After you construct the plug-in project put the file inside the project debug folder where the plug-in dll resides and import the solution using the registration tool. The solution xml contains step information for the Incident post create and update events as described above.



<Register LogFile="Plug-in Registration Log.txt" Server="http://moss:5555" Org="MicrosoftCRM" Domain="" UserName="administrator">
<Solution SourceType="1" Assembly="GenericConcatPlugIn.dll" Id="fc137b56-936f-4711-852c-d5d4ca508f73">
<PluginTypes>
<Plugin TypeName="GenericConcatPlugIn.Handler" FriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" Id="2545c234-dee8-4a4c-979f-749748358295">
<Steps>
<Step PluginTypeName="GenericConcatPlugIn.Handler" PluginTypeFriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" CustomConfiguration="<Entity> <Target ID="title" Format="{0} - {1}"> <Field ID="subjectid" Type="Lookup"/> <Field ID="customerid" Type="Customer"/> </Target> </Entity> " SecureConfiguration="" Description="Create of incident in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Create" Mode="0" PrimaryEntityName="incident" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="c0fef31b-08fe-dd11-91f6-0003ff2d0264">
<Images>
<Image EntityAlias="Target" ImageType="1" MessagePropertyName="Id" Attributes="" Id="f0f9d623-0bfe-dd11-91f6-0003ff2d0264" />
</Images>
</Step>
<Step PluginTypeName="GenericConcatPlugIn.Handler" PluginTypeFriendlyName="b283b2c5-0375-404e-b070-bbb350cb9d24" CustomConfiguration="<Entity> <Target ID="title" Format="{0} - {1}"> <Field ID="subjectid" Type="Lookup"/> <Field ID="customerid" Type="Customer"/> </Target> </Entity> " SecureConfiguration="" Description="Update of incident in Parent Pipeline" FilteringAttributes="" ImpersonatingUserId="" InvocationSource="0" MessageName="Update" Mode="0" PrimaryEntityName="incident" SecondaryEntityName="none" Stage="50" SupportedDeployment="0" Rank="1" Id="c03e4a2e-08fe-dd11-91f6-0003ff2d0264">
<Images>
<Image EntityAlias="Target" ImageType="0" MessagePropertyName="Target" Attributes="" Id="8065c335-08fe-dd11-91f6-0003ff2d0264" />
</Images>
</Step>
</Steps>
</Plugin>
</PluginTypes>
</Solution>
</Register>



Incident Post Create And Post Entity Image




Incident Post Update And Post Entity Image





Good Luck

4 comments:

Rob said...

Hi Adi,

This is a really helpful post. I was wondering if you could explain the STRING on lines 21, 110, 165.

Regards
Rob

Adi Katz said...

It should be a String (not a STRING).

Anonymous said...

Hi Adi,

What a great Plugin! But there is an error when an empty lookup is included in the config. How can I change the code to catch this exception? In the case that there is an empty lookup, I just want to insert an empty string.

Thanks for your help!

firedave said...

If you want to concatenate type of Picklist you need to add the following to the case statement:

case "Picklist":
TargetValues.Add(((Picklist)Properties[fieldId]).name + "");
break;