Developing custom workflow activities enables us to merge our own requirements with the Microsoft CRM 4.0 workflow features. This custom workflow activity can be used as a workflow step within a CRM workflow for opportunities. To keep it simple we will only export the product names of all products related to an opportunity (opportunityproduct) to a text file and we will not define the complete workflow to cover the scenario described above.
The custom workflow activity will call a Web service method of Microsoft Dynamics CRM and use an input parameter that can be entered by the user as a workflow property within the CRM workflow form.
Prerequisites
These are the tools and components I used to build my own Microsoft Dynamics CRM custom workflow activity:
- Microsoft Visual Studio 2005
- Windows Workflow Foundation
- Visual Studio 2005 extensions for .NET Framework 3.0 (Windows Workflow Foundation)
- Microsoft Dynamics CRM 4.0 SDK: the following assemblies must be added to your project. They can be found in the SDK\Bin folder:
- Microsoft.Crm.Sdk.dll
- Microsoft.Crm.SdkTypeProxy.dll
- The Plug-in Developer Tool to register the workflow as a plug-in: see Microsoft Dynamics CRM SDK Documentation for more information how to build and use this tool
Building a Workflow Activity Library
If the Visual Studio 2005 extensions for .NET Framework 3.0 are installed, some additional project templates for workflows are provided:
- Choose the Workflow Activity Library template.
- Add a reference to Microsoft.Crm.Sdk.dll and Microsoft.Crm.SdkTypeProxy.dll located in the SDK\Bin folder to the project created.
You may want to rename the generated Activity1 class. Please do not use the same name as the namespace of your project because you might get problems when publishing this workflow in CRM. In this example I use ExportProductInformationActivity as the class name andExportProductInformation as the namespace.
Change to the code view of the generated class and you see that the generated class derives from SequenceActivity. This is an activity of the Windows Workflow Foundation base activity library which may run a set of child activities in an ordered manner. This is ok for our purpose even though we will not run any child activities.
To access the custom workflow activity from the Microsoft CRM 4.0 workflow interface you have to annotate the class with the .NET attribute CrmWorkflowActivity and override the Execute method which is called by the workflow runtime to execute an activity. Basically, that is all you have to do to implement a custom workflow activity for Microsoft CRM 4.0.
In our scenario we want to access the CRM Web service methods to get information about the opportunityproducts related to the opportunity for which the workflow is started.
Use the ActivityExecutionContext passed to the Execute method to get the workflow context, which contains information about the instance of the workflow.
Basically you will get information about the primary entity (name, id, pre- and post-image) using the workflow context, but it also contains a method CreateCrmService which returns a reference to the CrmService. Please note that this is a reference to the CrmService of the SDK assembly, we added to the project, so you do not have access to any custom fields or entities. Use CRM's MetadataService Web service to access them. According to the CrmService there is a CreateMetadataService method to get a reference to the MetadataService.
Here is the code of ExportProductInformationActivity.cs:
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Workflow;
using System.IO;
namespace ExportProductInformation
{
/// <summary>
/// Custom workflow activity class derived from SequenceActivity.
/// The CrmWorkflowActivity provides the information needed by the CRM workflow form
/// </summary>
[CrmWorkflowActivity("Export Product Information", "My Custom Workflow Activities")]
public partial class ExportProductInformationActivity : SequenceActivity
{
/// <summary>
/// The dependency property is used to define an input parameter
/// It is annotated with attribute CrmInput to access the parameter within the CRM workflow form
/// The parameter of Attribute CrmDefault is used as the default content of the property.
/// </summary>
public static DependencyProperty filenameProperty = DependencyProperty.Register("filename", typeof(string), typeof(ExportProductInformationActivity));
[CrmInput("Filename")]
[CrmDefault(@"C:\Test\OrderForecast.txt")]
public string filename
{
get
{
return (string)base.GetValue(filenameProperty);
}
set
{
base.SetValue(filenameProperty, value);
}
}
public ExportProductInformationActivity()
{
InitializeComponent();
}
/// <summary>
/// The Execute method is called by the workflow runtime to execute an activity.
/// </summary>
/// <param name="executionContext"> The context for the activity</param>
/// <returns></returns>
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
// Get the context service.
IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;
// Use the context service to create an instance of CrmService.
ICrmService crmService = context.CreateCrmService(true);
BusinessEntityCollection opportunityProducts = RetrieveOpportunityProducts(crmService, context.PrimaryEntityId);
// Use the given filename to create a new StreamWriter
TextWriter w = new StreamWriter(this.filename);
string productname;
foreach (DynamicEntity de in opportunityProducts.BusinessEntities)
{
productname = de.Properties.Contains("productid") ? ((Lookup)de.Properties["productid"]).name : null;
w.WriteLine(productname);
}
w.Close();
return ActivityExecutionStatus.Closed;
}
private BusinessEntityCollection RetrieveOpportunityProducts(ICrmService service, Guid oppId)
{
Microsoft.Crm.Sdk.Query.QueryByAttribute query = new Microsoft.Crm.Sdk.Query.QueryByAttribute();
query.ColumnSet = new Microsoft.Crm.Sdk.Query.AllColumns();
query.EntityName = EntityName.opportunityproduct.ToString();
query.Attributes = new string[] { "opportunityid" };
query.Values = new string[] { oppId.ToString() };
RetrieveMultipleRequest retrieve = new RetrieveMultipleRequest();
retrieve.Query = query;
retrieve.ReturnDynamicEntities = true;
RetrieveMultipleResponse retrieved = (RetrieveMultipleResponse)service.Execute(retrieve);
return retrieved.BusinessEntityCollection;
}
}
}
You can noticed that the code contains the definition of a DependencyProperty named filenameProperty. In Windows Workflow Foundation Properties provided on activities are used to make the activity configurable by the user respectively the consumer of the activity. In our case the CRM user should be able to decide the location where the generated export file will be saved. The property is annotated with the CRMInput attribute to be able to access it as a workflow property on the CRM workflow form as an input parameter. The CrmDefaultattribute is used to set a default value.
All plug-in assemblies used with Microsoft CRM 4.0 have to be signed. So we have to sign the workflow assembly, too, as it will be registered as a plug-in. To sign the assembly with a strong name, go to the project properties of the Visual Studio project, tab 'Signing', check 'Sign the assembly'. Under 'Choose a strong name key file' choose <New> and provide a name for the key file.
Registering the Workflow Activity as a Plug-in
After your custom workflow activity has been compiled, you have to register this assembly as a plug-in. Your activity will then appear in the workflow form in the Microsoft Dynamics CRM Web application.
I used the SDK's Plug-in Developer Tool to register the workflow activity using the following register.xml:
<?xml version="1.0" encoding="utf-8" ?>
<Register
LogFile = "Plug-in Registration Log.txt"
Server = "http://[your CRM server:port]"
Org = "[your organization name]"
Domain = "[your domain]"
UserName= "[your user name]" >
<Solution SourceType="0" Assembly="[location of the assembly dll]">
<WorkflowTypes>
<WorkflowType
TypeName="ExportProductInformation.ExportProductInformationActivity"
FriendlyName="Export Product Information"/>
</WorkflowTypes>
</Solution>
</Register>
If you run the Plug-in Developer Tool multiple times to register the assemblies after you changed something you might have to restart IIS.
Testing the Custom Workflow Activity
The Custom Workflow Activity is now registered and if you open the Workflows and create a new one for opportunities, you can add your activity as a workflow step:
Make sure you have created an appropriate directory at CRM Server to save the output file to as there is no error handling implemented. Saving the file to C:\ is not the best option.
To keep it simple we choose the workflow to be available to run On Demand. Please do not forget to uncheck the option Record is created under Start when.
After publishing the workflow you can start it by selecting an opportunity and using button Run Workflow.
Debugging the Custom Workflow Activity
To Debug the custom workflow activity you have to copy the pdb file of you assembly to %installdir%\server\bin\assembly on your CRM server, set a breakpoint in your Execute method and attach your Visual Studio project to Microsoft Dynamics CRM asynchronous service (processCrmAsyncService.exe) on your CRM Server.
If you have difficulties to update the file in the assembly directory on the server because there is already another copy of the file run iisreset in a command window and / or restart the Microsoft Dynamics CRM asynchronous service.