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 DictionaryTargets;
///
/// 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 DictionaryRelated;
#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(DictionaryTargets)
{
#region Validate Target Entity
if (!this.Context.InputParameters.Contains(ParameterName.Target))
{
return;
}
#endregion
#region Concatenate Target Fields
DynamicEntity Current = null;
foreach (KeyValuePairTarget 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)
{
ListTargetValues = new List ();
StringProperty TargetProperty = new StringProperty();
TargetProperty.Name = Target.ID;
#region Retrieve Each Concatenated Field Value
foreach (KeyValuePairrelated 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
6 comments:
Hi Adi,
This is a really helpful post. I was wondering if you could explain the STRING on lines 21, 110, 165.
Regards
Rob
It should be a String (not a STRING).
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!
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;
Hiya, I’m really glad I’ve found this information. Nowadays bloggers publish just about gossip and net stuff and this is really irritating. A good web site with interesting content, this is what I need. Thank you for making this web-site, and I will be visiting again. Do you do newsletters by email?
most profitable items to sell on ebay
Great insights on using the CRM 4.0 Concatenating Fields plug-in! This makes streamlining data entry so much easier, enhancing efficiency in record management. Thanks for sharing this valuable tip
Digital Marketing Course In Hyderabad
Digital Marketing Course In Ameerpet
Post a Comment