Programming Microsoft Dynamics CRM 4.0: Plug-ins
- 12/15/2008
Sample Plug-ins
Now that you have a good understanding of how plug-ins work, let’s dig into some real-world examples. We will cover three different plug-in examples:
Rolling up child entity attributes to a parent entity
System view hider
Customization change notifier
All these examples include source code that you can examine and use in your Microsoft Dynamics CRM deployment.
Rolling Up Child Entity Attributes to a Parent Entity
Frequently you will encounter a request to include some information in a view, such as the number of active contacts for a particular account or the next activity due date on a lead. You can accomplish this by writing a plug-in in a generic way so that it can handle all the messages involved in modifying a child record. The next example keeps track of the next scheduled phone call’s scheduledstart value and stores it in a custom attribute on the related lead.
Start by adding a new class named NextPhoneCallUpdater to the ProgrammingWithDynamicsCrm4.Plugins project. Then stub out the class to match Example 5-20.
Example 5-20. The start of the NextPhoneCallUpdater plug-in
using System; using Microsoft.Crm.Sdk; using Microsoft.Crm.Sdk.Query; using Microsoft.Crm.SdkTypeProxy; using ProgrammingWithDynamicsCrm4.Plugins.Attributes; namespace ProgrammingWithDynamicsCrm4.Plugins { public class NextPhoneCallUpdater : IPlugin { public void Execute(IPluginExecutionContext context) { } } }
The first thing we need to think about is which messages our plug-in needs to register for. It needs to listen to Create and Delete messages for a phonecall. It also needs to listen to Update messages in case the scheduledstart attribute is changed or the regardingobjectid is changed. Finally, it needs to listen to the SetState and SetStateDynamicEntity messages to detect when the phonecall is marked as Complete or Cancelled. SetState and SetStateDynamicEntity are two different messages that accomplish the same thing, but you need to listen for both if you want to handle updates from the Web service API and from the CRM application. Based on this information we can add our PluginStep and PluginImage attributes to our class definition as shown in Example 5-21.
Example 5-21. The PluginStep and PluginImage attributes for the NextPhoneCallUpdater plug-in
[PluginStep("Create", PluginStepStage.PostEvent, PrimaryEntityName = "phonecall", StepId = "PhoneCallPostCreate")] [PluginImage(ImageType.PostImage, "PhoneCallPostCreate", "Id", "PhoneCall")] [PluginStep("Update", PluginStepStage.PostEvent, PrimaryEntityName = "phonecall", StepId = "PhoneCallPostUpdate")] [PluginImage(ImageType.Both, "PhoneCallPostUpdate", "Target", "PhoneCall")] [PluginStep("Delete", PluginStepStage.PostEvent, PrimaryEntityName = "phonecall", StepId = "PhoneCallPostDelete")] [PluginImage(ImageType.PreImage, "PhoneCallPostDelete", "Target", "PhoneCall")] [PluginStep("SetState", PluginStepStage.PostEvent, PrimaryEntityName = "phonecall", StepId = "PhoneCallPostSetState")] [PluginImage(ImageType.Both, "PhoneCallPostSetState", "EntityMoniker", "PhoneCall")] [PluginStep("SetStateDynamicEntity", PluginStepStage.PostEvent, PrimaryEntityName = "phonecall", StepId = "PhoneCallPostSetStateDynamicEntity")] [PluginImage(ImageType.Both, "PhoneCallPostSetStateDynamicEntity", "EntityMoniker", "PhoneCall")] public class NextPhoneCallUpdater : IPlugin { ... }
This probably looks like a lot of code to register for the appropriate messages, and it is. However, when you are keeping track of information on a child entity you need to account for all of the scenarios that could change your calculated value, and register messages accordingly. Therefore, you often register for these same messages whenever you need to populate one of these rolled-up attributes.
Also note the images that we set up for each step. Create gets a post-image, Delete gets a pre-image, and the rest get both types of images. The values we pass in for MessagePropertyName on the images come from Table 5-5.
The Execute method needs to determine which lead the phonecall is associated with in the pre-image and which it is associated with in the post-image. If they are different, both need to be updated. If they are the same, only that single lead will be updated. The Execute method source code is shown in Example 5-22.
Example 5-22. NextPhoneCallUpdater’s Execute method
public void Execute(IPluginExecutionContext context) { Guid preLeadId = GetRegardingLeadId(context.PreEntityImages, "PhoneCall"); Guid postLeadId = GetRegardingLeadId(context.PostEntityImages, "PhoneCall"); ICrmService crmService = context.CreateCrmService(true); UpdateNextCallDueDate(crmService, preLeadId); if (preLeadId != postLeadId) { UpdateNextCallDueDate(crmService, postLeadId); } }
The Execute method is fairly easy to understand, but it passes off most of the work to two additional methods, GetRegardingLeadId and UpdateNextCallDueDate. Let’s start by taking a look at GetRegardingLeadId in Example 5-23.
Example 5-23. The GetRegardingLeadId method
private Guid GetRegardingLeadId(PropertyBag images, string entityAlias) { Guid regardingLeadId = Guid.Empty; if (images.Contains(entityAlias)) { DynamicEntity entity = (DynamicEntity)images[entityAlias]; if (entity.Properties.Contains("regardingobjectid")) { Lookup regardingObjectId = (Lookup)entity["regardingobjectid"]; if (regardingObjectId.type == "lead") { regardingLeadId = regardingObjectId.Value; } } } return regardingLeadId; }
Because not all messages have a pre-image and a post-image, we wrote this method to be forgiving if the image is not found. If the phone call image is found and the regardingobjectid attribute is associated with a lead, the method returns the Guid from regardingobjectid. Otherwise, it returns an empty Guid.
Once the lead IDs are identified, we need to update the attribute on the correspondingleads. UpdateNextCallDueDate is responsible for performing this functionality. Example 5-24 is the full source code for the UpdateNextCallDueDate method.
Example 5-24. The UpdateNextCallDueDate method
private void UpdateNextCallDueDate(ICrmService crmService, Guid leadId) { if (leadId != Guid.Empty) { DynamicEntity lead = new DynamicEntity("lead"); lead["leadid"] = new Key(leadId); DynamicEntity phoneCall = GetNextScheduledPhoneCallForLead(crmService, leadId); if (phoneCall != null) { lead["new_nextphonecallscheduledat"] = phoneCall["scheduledstart"]; } else { lead["new_nextphonecallscheduledat"] = CrmDateTime.Null; } crmService.Update(lead); } }
UpdateNextCallDueDate is responsible for updating the custom new_nextphonecallescheduledat attribute on the lead. It sets it to the earliest scheduledstart value for phone calls associated with this lead. If no phone calls are associated with the lead (or they do not have scheduledstart values), it nulls out the new_nextphonecallscheduledat attribute on the lead.
UpdateNextCallDueDate calls one additional method, GetNextScheduledPhoneCallForLead, to determine which phone call it should use. The source code for GetNextScheduledPhoneCall-ForLead is displayed in Example 5-25.
Example 5-25. The GetNextScheduledPhoneCallForLead method
private DynamicEntity GetNextScheduledPhoneCallForLead( ICrmService crmService, Guid leadId) { QueryExpression query = new QueryExpression(); query.EntityName = EntityName.phonecall.ToString(); ColumnSet cols = new ColumnSet(new string[] { "scheduledstart" }); query.ColumnSet = cols; FilterExpression filter = new FilterExpression(); query.Criteria = filter; ConditionExpression regardingCondition = new ConditionExpression(); regardingCondition.AttributeName = "regardingobjectid"; regardingCondition.Operator = ConditionOperator.Equal; regardingCondition.Values = new object[] { leadId }; filter.Conditions.Add(regardingCondition); ConditionExpression activeCondition = new ConditionExpression(); activeCondition.AttributeName = "statecode"; activeCondition.Operator = ConditionOperator.Equal; activeCondition.Values = new object[] { "Open" }; filter.Conditions.Add(activeCondition); ConditionExpression scheduledCondition = new ConditionExpression(); scheduledCondition.AttributeName = "scheduledstart"; scheduledCondition.Operator = ConditionOperator.NotNull; filter.Conditions.Add(scheduledCondition); query.PageInfo = new PagingInfo(); query.PageInfo.Count = 1; query.PageInfo.PageNumber = 1; query.AddOrder("scheduledstart", OrderType.Ascending); RetrieveMultipleRequest request = new RetrieveMultipleRequest(); request.Query = query; request.ReturnDynamicEntities = true; RetrieveMultipleResponse response; response = (RetrieveMultipleResponse)crmService.Execute(request); DynamicEntity phoneCall = null; if (response.BusinessEntityCollection.BusinessEntities.Count == 1) { phoneCall = (DynamicEntity) response.BusinessEntityCollection.BusinessEntities[0]; } return phoneCall; }
GetNextScheduledPhoneCallForLead constructs a QueryExpression that filters for active phonecall entities that are associated with the specified lead and have a scheduledstart value. The query is set to return only one record and is sorted by the scheduledstart attribute in ascending order. If no matching phonecall is found, it returns null.
The end result is that regardless of how a phonecall entity is created, updated, or deleted, the parent entity’s attribute will always be recalculated.
System View Hider
Microsoft Dynamics CRM includes default system views for entities such as accounts, contacts, and others. You might find that your organization does not want to use all these system views, and therefore you’d like to remove one or more of them. Unfortunately, if you try to customize the entity with the Web interface to delete a system view, you will receive an error message stating that you cannot remove a system view.
Fortunately, however, you can use a plug-in to hide specific system views from your users. For this sample, let’s assume we want to hide the No Orders in Last 6 Months system view on both the account and contact entities. The plug-in can do this because CRM queries for the list of systemview entities associated with a particular entity when displaying the picklist of views. This plug-in example is fairly straightforward to implement, so let’s start by looking at the completed code in Example 5-26.
Example 5-26. SystemViewHider plug-in source code
using System; using Microsoft.Crm.Sdk; using Microsoft.Crm.Sdk.Query; using Microsoft.Crm.SdkTypeProxy; using ProgrammingWithDynamicsCrm4.Plugins.Attributes; namespace ProgrammingWithDynamicsCrm4.Plugins { [PluginStep("RetrieveMultiple", PluginStepStage.PreEvent, PrimaryEntityName="savedquery")] public class SystemViewHider : IPlugin { public void Execute(IPluginExecutionContext context) { object[] systemViews = new object[] { //Contacts: No Orders in Last 6 Months new Guid("9818766E-7172-4D59-9279-013835C3DECD"), //Accounts: No Orders in Last 6 Months new Guid("C147F1F7-1D78-4D10-85BF-7E03B79F74FA"), }; if (context.InputParameters != null && systemViews.Length > 0) { if (context.InputParameters.Properties.Contains( ParameterName.Query)) { QueryExpression query; query = (QueryExpression) context.InputParameters[ParameterName.Query]; if (query.EntityName == EntityName.savedquery.ToString()) { if (query.Criteria != null) { if (query.Criteria.Conditions != null) { ConditionExpression condition = new ConditionExpression(); condition.AttributeName = "savedqueryid"; condition.Operator = ConditionOperator.NotIn; condition.Values = systemViews; query.Criteria.Conditions.Add(condition); context.InputParameters[ParameterName.Query] = query; } } } } } } }
The first thing to notice is this plug-in takes advantage of a message that might not be as obvious as some that we have dealt with in the past. The RetrieveMultiple message is a valid message to register for, and as is shown here you can manipulate the QueryExpression being passed to it before the core operation is executed.
The other factor that allows this plug-in to work is that the system view IDs for native entities are always the same across CRM installations. If this were not the case, we would need to specify the view IDs during registration in the customconfiguration attribute for the plug-in step or perform a query within the plug-in to find the right view ID to exclude.
Customization Change Notifier
Customers often ask how they can keep track of customization changes in a development environment, or even for auditing in a production environment. If multiple people possess system administrator privileges, they could be making customization changes to the system at the same time. This might cause confusion or problems, especially if multiple users work with the same entity at the same time.
Obviously a good software development process dictates that developers and system administrators should communicate and follow a strict process when making changes to any environment. However, we created a sample plug-in that records customization changes. The plug-in presented in this sample doesn’t prevent two users from working on the same records at the same time, but you can use it in conjunction with your development process as a safety net.
The CustomizationChangeNotifier plug-in listens for the Publish and PublishAll messages. To specify which users to notify of customization changes, we have added a custom Boolean attribute named new_receivecustomizationnotifications on the systemuser entity. By checking the corresponding check box on the systemuser form, the user is included in the notification e-mails. This plug-in differs from previous samples because it subscribes to both the pre-event and post-event steps and passes information between the two steps. Example 5-27 shows the start of the CustomizationChangeNotifier code.
Example 5-27. CustomizationChangeNotifier
using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Xml; using System.Xml.Xsl; using Microsoft.Crm.Sdk; using Microsoft.Crm.Sdk.Query; using Microsoft.Crm.SdkTypeProxy; using ProgrammingWithDynamicsCrm4.Plugins.Attributes; namespace ProgrammingWithDynamicsCrm4.Plugins { [PluginStep("Publish", PluginStepStage.PreEvent)] [PluginStep("Publish", PluginStepStage.PostEvent)] [PluginStep("PublishAll", PluginStepStage.PreEvent)] [PluginStep("PublishAll", PluginStepStage.PostEvent)] public class CustomizationChangeNotifier : IPlugin { public void Execute(IPluginExecutionContext context) { } } }
So far everything looks pretty normal, with the exception of the already mentioned fact that we registered this plug-in for both the pre-event and post-event steps. Example 5-28 fills out the Execute method, and things start to get interesting.
Example 5-28. CustomizationChangeNotifier’s Execute method
public void Execute(IPluginExecutionContext context) { if (context.Stage == MessageProcessingStage.BeforeMainOperationOutsideTransaction) { byte[] preXml = GetCustomizationSnapshot(context); context.SharedVariables["CustomizationChangeNotifier.PreXml"] = preXml; } else { SendNotification(context, (byte[])context.SharedVariables["CustomizationChangeNotifier.PreXml"]); } }
Because this plug-in executes in two different steps it needs to determine which step it is executing in right away and call the appropriate method. During the pre-event step, this plug-in grabs a snapshot of the customizations and stores them in the context’s SharedVariables PropertyBag. Then, during the post-event step, it gets that customization snapshot out of SharedVariables and passes it on to the SendNotification method.
SharedVariables is shared by all plug-ins within a pipeline. Because of this, you should be sure the keys you use are likely to be unique. The only reason we use a byte array here is to deal with the compressed version of the customization data. It is also worth mentioning that we could have implemented this plug-in as two different plug-ins, each registered for its own step, but both steps have enough shared functionality that it made sense to use a single plug-in class. Let’s examine the source code for GetCustomizationSnapshot in Example 5-29.
Example 5-29. GetCustomizationSnapshot
private byte[] GetCustomizationSnapshot(IPluginExecutionContext context) { ICrmService crmService = context.CreateCrmService(true); if (context.MessageName == "Publish") { ExportCompressedXmlRequest request = new ExportCompressedXmlRequest(); string parameterXml = (string)context.InputParameters["ParameterXml"]; request.ParameterXml = TransformParameterXmlToExportXml(parameterXml); request.EmbeddedFileName = "customizations.xml"; ExportCompressedXmlResponse response = (ExportCompressedXmlResponse)crmService.Execute(request); return response.ExportCompressedXml; } else { ExportCompressedAllXmlRequest request = new ExportCompressedAllXmlRequest(); request.EmbeddedFileName = "customizations.xml"; ExportCompressedAllXmlResponse response = (ExportCompressedAllXmlResponse)crmService.Execute(request); return response.ExportCompressedXml; } }
If you recall, not only did we register this plug-in for two steps, but we also registered it for two messages. Depending on whether the message is Publish or PublishAll, the plug-in will either get a subset of the current customizations or all of them. The two messages, ExportCompressedXml and ExportCompressedAllXml, are used to get the customization changes from CRM. The EmbeddedFileName property is used to specify the name of the file that is embedded in the zip file that is returned.
Unfortunately, the ParameterXml passed in through the context’s InputParameters PropertyBag is not quite the same as what is required by the ExportCompressedXml message. ExportCompressedXml requires all of the root node’s children (entities, nodes, security roles, workflows, and settings) even if you are not asking for any of those customization types. The ParameterXml only contains the customizations that are being published. Because of this slight difference, we need to do a transformation on the XML as shown in Example 5-30.
Example 5-30. The TransformParameterXmlToExportXml method
private string TransformParameterXmlToExportXml(string parameterXml) { string xsl = @" <xsl:transform version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:template match='/'> <importexportxml> <entities> <xsl:apply-templates select='publish/entities/entity' /> </entities> <nodes> <xsl:apply-templates select='publish/nodes/node' /> </nodes> <securityroles> <xsl:apply-templates select='publish/securityroles/securityrole' /> </securityroles> <workflows> <xsl:apply-templates select='publish/workflows/workflow' /> </workflows> <settings> <xsl:apply-templates select='publish/settings/setting' /> </settings> </importexportxml> </xsl:template> <xsl:template match='@*|node()'> <xsl:copy> <xsl:apply-templates select='@*|node()'/> </xsl:copy> </xsl:template> </xsl:transform>"; XslCompiledTransform transform = new XslCompiledTransform(); transform.Load(XmlReader.Create(new StringReader(xsl))); XmlTextReader publishXmlReader = new XmlTextReader(new StringReader(parameterXml)); publishXmlReader.Namespaces = false; StringBuilder results = new StringBuilder(); XmlWriter resultsWriter = XmlWriter.Create(results); transform.Transform(publishXmlReader, null, resultsWriter); return results.ToString(); }
Most of this method is just the declaration of the XSLT. While the specific details of the XSLT are outside the scope of this book, an abundance of information is available about XSLT both in books and on the Internet. The rest of the code in this method is simply using the XSLT to transform the ParameterXml passed in to the return value, which is passed to the ExportCompressedXmlRequest.
As shown back in Example 5-28, when the Execute method is called for the post-event step, it passes along the plug-in context and the compressed XML from the pre-event step to the SendNotification method. The source code for the SendNotification method is displayed in Example 5-31.
Example 5-31. The SendNotification method
private void SendNotification(IPluginExecutionContext context, byte[] preXml) { ICrmService crmService = context.CreateCrmService(true); activityparty[] recipients = GetNotifcationRecipients(crmService); if (recipients.Length > 0) { byte[] postXml = GetCustomizationSnapshot(context); email email = new email(); email.from = new activityparty[1]; email.from[0] = new activityparty(); email.from[0].partyid = new Lookup("systemuser", context.UserId); email.subject = "Customization Notification"; email.description = @"You are receiving this email because a customization change has been published."; email.to = recipients; Guid emailId = crmService.Create(email); emailId = CreateEmailAttachment(crmService, emailId, preXml, "PreCustomizations.zip", "application/zip", 1); emailId = CreateEmailAttachment(crmService, emailId, postXml, "PostCustomizations.zip", "application/zip", 2); SendEmailRequest sendRequest = new SendEmailRequest(); sendRequest.EmailId = emailId; sendRequest.IssueSend = true; sendRequest.TrackingToken = String.Empty; crmService.Execute(sendRequest); } }
SendNotification starts by getting a list of recipients that have indicated they want to be notified of customization changes. As long as at least one user has indicated that he or she would like to receive change notifications, another snapshot of the customizations is taken that can be used to compare against the customizations that are captured in the pre-event step. An e-mail message is then prepared, including both snapshots of the customization files as attachments, and sent to the list of recipients.
GetNotificationRecipients, as shown in Example 5-32, queries to find which system users have the custom attribute new_receivecustomizationnotifications set to true and returns an array of activityparty instances that reference them.
Example 5-32. The GetNotificationRecipients method
private activityparty[] GetNotifcationRecipients(ICrmService crmService) { QueryByAttribute query = new QueryByAttribute(); query.EntityName = "systemuser"; query.ColumnSet = new ColumnSet(new string[] { "systemuserid" }); query.Attributes = new string[] { "new_receivecustomizationnotifications" }; query.Values = new object[] { true }; RetrieveMultipleRequest request = new RetrieveMultipleRequest(); request.Query = query; request.ReturnDynamicEntities = true; RetrieveMultipleResponse response; response = (RetrieveMultipleResponse)crmService.Execute(request); List<BusinessEntity> systemUsers = response.BusinessEntityCollection.BusinessEntities; List<activityparty> recipients = new List<activityparty>(); foreach (DynamicEntity systemUser in systemUsers) { activityparty recipient = new activityparty(); Guid systemUserId = ((Key)systemUser["systemuserid"]).Value; recipient.partyid = new Lookup("systemuser", systemUserId); recipients.Add(recipient); } return recipients.ToArray(); }
The last remaining piece of code is the CreateEmailAttachment method, which is displayed in Example 5-33. As the name implies, this method creates an attachment for an e-mail message in CRM.
Example 5-33. The CreateEmailAttachment method
private static Guid CreateEmailAttachment(ICrmService crmService, Guid emailId, byte[] data, string filename, string mimeType, int attachmentNumber ) { activitymimeattachment emailAttachment = new activitymimeattachment(); emailAttachment.activityid = new Lookup("email", emailId); emailAttachment.attachmentnumber = new CrmNumber(attachmentNumber); emailAttachment.mimetype = mimeType; emailAttachment.body = Convert.ToBase64String(data); emailAttachment.filename = filename; crmService.Create(emailAttachment); return emailId; }
This sample demonstrates some of the more creative and powerful uses of plug-ins and SharedVariables as well as illustrating how to send an e-mail message with attachments using the CRM service.