Showing posts with label Workflow. Show all posts
Showing posts with label Workflow. Show all posts

Return a Calculated Value

The following sample workflow activity demonstrates how to return a calculated value from an activity.

 
using System;
using System.Collections;
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 System.Reflection;

using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace SampleWorkflows
{
[CrmWorkflowActivity("Return a Calculated Value")]
public class AddActivity : Activity
{

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
result = new CrmNumber(a.Value + b.Value);
return base.Execute(executionContext);
}

public static DependencyProperty aProperty =
DependencyProperty.Register("a",
typeof(CrmNumber),
typeof(AddActivity));

[CrmInput("a")]
public CrmNumber a
{
get
{
return (CrmNumber)base.GetValue(aProperty);
}
set
{
base.SetValue(aProperty, value);
}

}

public static DependencyProperty bProperty =
DependencyProperty.Register("b",
typeof(CrmNumber),
typeof(AddActivity));

[CrmInput("b")]
public CrmNumber b
{
get
{
return (CrmNumber)base.GetValue(bProperty);
}
set
{
base.SetValue(bProperty, value);
}

}

public static DependencyProperty resultProperty =
DependencyProperty.Register("result",
typeof(CrmNumber),
typeof(AddActivity));

[CrmOutput("result")]
public CrmNumber result
{
get
{
return (CrmNumber)base.GetValue(resultProperty);
}
set
{
base.SetValue(resultProperty, value);
}

}
}
}




Use of CRM Types in Custom Workflow Activity

The following sample shows how to use each of the types found in Microsoft Dynamics CRM.

Input Parameter: Number

Example


public static DependencyProperty myNumberProperty = DependencyProperty.Register("myNumber", typeof(CrmNumber), typeof(CreateCustomEntity));

[CrmInput("My Integer")]
public CrmNumber myNumber
{
get
{
return (CrmNumber)base.GetValue(myNumberProperty);
}
set
{
base.SetValue(myNumberProperty, value);
}
}


Input Parameter: String


Example



public static DependencyProperty myStringProperty = DependencyProperty.Register("myString", typeof(System.String), typeof(CreateCustomEntity));

[CrmInput("My String")]
public string myString
{
get
{
return (string)base.GetValue(myStringProperty);
}
set
{
base.SetValue(myStringProperty, value);
}
}


Input Parameter: Boolean


Example


public static DependencyProperty myBooleanProperty = DependencyProperty.Register("myBoolean", typeof(CrmBoolean), typeof(CreateCustomEntity));

[CrmInput("My Boolean")]
public CrmBoolean myBoolean
{
get
{
return (CrmBoolean)base.GetValue(myBooleanProperty);
}
set
{
base.SetValue(myBooleanProperty, value);
}
}


Input Parameter: Lookup


Example



public static DependencyProperty myLookupProperty = DependencyProperty.Register("myLookup", typeof(Lookup), typeof(CreateCustomEntity));

[CrmInput("My Lookup")]
[CrmReferenceTarget("account")]
public Lookup myLookup
{
get
{
return (Lookup)base.GetValue(myLookupProperty);
}
set
{
base.SetValue(myLookupProperty, value);
}
}


Input Parameter: Picklist


Example



public static DependencyProperty myPicklistProperty = DependencyProperty.Register("myPicklist", typeof(Picklist), typeof(CreateCustomEntity));

[CrmInput("My Picklist")]
[CrmAttributeTarget("account", "industrycode")]
public Picklist myPicklist
{
get
{
return (Picklist)base.GetValue(myPicklistProperty);
}
set
{
base.SetValue(myPicklistProperty, value);
}
}


Input Parameter: DateTime


Example



public static DependencyProperty myDateTimeProperty = DependencyProperty.Register("myDateTime", typeof(CrmDateTime), typeof(CreateCustomEntity));

[CrmInput("My DateTime")]
public CrmDateTime myDateTime
{
get
{
return (CrmDateTime)base.GetValue(myDateTimeProperty);
}
set
{
base.SetValue(myDateTimeProperty, value);
}
}


Input Parameter: Decimal


Example



public static DependencyProperty myDecimalProperty = DependencyProperty.Register("myDecimal", typeof(CrmDecimal), typeof(CreateCustomEntity));

[CrmInput("My Decimal")]
public CrmDecimal myDecimal
{
get
{
return (CrmDecimal)base.GetValue(myDecimalProperty);
}
set
{
base.SetValue(myDecimalProperty, value);
}
}


Input Parameter: Money


Example


public static DependencyProperty myMoneyProperty = DependencyProperty.Register("myMoney", typeof(CrmMoney), typeof(CreateCustomEntity));

[CrmInput("My Money")]
public CrmMoney myMoney
{
get
{
return (CrmMoney)base.GetValue(myMoneyProperty);
}
set
{
base.SetValue(myMoneyProperty, value);
}
}


Input Parameter: Float


Example


public static DependencyProperty myFloatProperty = DependencyProperty.Register("myFloat", typeof(CrmFloat), typeof(CreateCustomEntity));

[CrmInput("My Float")]
public CrmFloat myFloat
{
get
{
return (CrmFloat)base.GetValue(myFloatProperty);
}
set
{
base.SetValue(myFloatProperty, value);
}
}


Input Parameter: Status


Example


public static DependencyProperty myStatusProperty = DependencyProperty.Register("myStatus", typeof(Status), typeof(CreateCustomEntity));

[CrmInput("My Status")]
[CrmAttributeTarget("account", "statuscode")]
public Status myStatus
{
get
{
return (Status)base.GetValue(myStatusProperty);
}
set
{
base.SetValue(myStatusProperty, value);
}
}

Output Parameter: Lookup


Example


public static DependencyProperty myOutLookupProperty = DependencyProperty.Register("myOutLookup", typeof(Lookup), typeof(CreateCustomEntity));

[CrmOutput("My Output Lookup")]
[CrmReferenceTarget("new_customentity")]
public Lookup myOutLookup
{
get
{
return (Lookup)base.GetValue(myOutLookupProperty);
}
set
{
base.SetValue(myOutLookupProperty, value);
}
}

Execute Method: Using Types


Example


protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{

IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;

ICrmService crmService = (ICrmService)context.CreateCrmService();

DynamicEntity de = new DynamicEntity("new_customentity");

if (myBoolean != null)
{
de["new_boolean"] = myBoolean;
}
if (myDateTime != null)
{
de["new_datetime"] = myDateTime;
}
if (myDecimal != null)
{
de["new_decimal"] = myDecimal.Value.ToString();
}
if (myFloat != null)
{
de["new_float"] = myFloat;
}

if (myLookup != null)
{
de["new_lookup"] = myLookup;
}
if (myMoney != null)
{
de["new_money"] = myMoney;
}
if (myNumber != null)
{
de["new_number"] = myNumber;
}
if (myPicklist != null)
{
de["new_picklist"] = myPicklist;
}
if (myStatus != null)
{
de["new_status"] = myStatus.Value.ToString();
}
if (myString != null)
{
de["new_stringtext"] = myString;
de["new_memo"] = myString;
}

de["new_activationid"] = new Lookup(EntityName.workflow.ToString(),context.ActivationId);

myOutLookup = new Lookup("new_customentity", crmService.Create(de));
return ActivityExecutionStatus.Closed;
}

Get the Next Birthday

The following sample workflow activity returns the next birthday. Use this in a workflow that sends a birthday greeting to a customer. Note that this uses dynamic entity rather than strong types as is recommended for workflows and plug-ins.



using System;
using System.Collections;
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 System.Reflection;
using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace SampleWorkflows
{
///
/// Activity will return the next upcoming birthday that has just passed
///
/// If this year's birthday has not yet occurred, it will return this year's birthday.
/// Otherwise, it will return the birthday for next year.
///
/// A workflow can time-out when on this date.
///

[CrmWorkflowActivity("Get the Next Birthday", "Release Scenarios")]
[PersistOnClose]
public partial class UpdateNextBirthday : SequenceActivity
{
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;

//Create a CRM Service.
ICrmService crmService = context.CreateCrmService();

//Retrieve the contact ID.
Guid contactId = ((Lookup)base.GetValue(ContactProperty)).Value;

//Retrieve the entity to determine what the birth date is.
//Retrieve the Contact Entity
DynamicEntity contactEntity;
{
//Create the target.
TargetRetrieveDynamic retrieveTarget = new TargetRetrieveDynamic();
retrieveTarget.EntityId = contactId;
retrieveTarget.EntityName = EntityName.contact.ToString();

//Create a request.
RetrieveRequest retrieveRequest = new RetrieveRequest();
retrieveRequest.ColumnSet = new ColumnSet(new string[] {"birthdate" });
retrieveRequest.ReturnDynamicEntities = true;
retrieveRequest.Target = retrieveTarget;

//Execute the request.
RetrieveResponse retrieveResponse = (RetrieveResponse)crmService.Execute(retrieveRequest);

//Retrieve the Loan Application Entity.
contactEntity = retrieveResponse.BusinessEntity as DynamicEntity;
}

//Check to see if the current birthday is set. We don't want the activity to fail if the birth date is not set.
CrmDateTime contactBirthDate = (CrmDateTime)contactEntity["birthdate"];
if (contactBirthDate == null || (contactBirthDate.UniversalTime == null))
{
//Complete the execution of the activity.
return base.Execute(executionContext);
}

//Calculate the next birth date. Encapsulate it in a method so that the method can be used in the test case for verification purposes.
DateTime nextBirthdate = CalculateNextBirthday(contactBirthDate.UniversalTime);

//Update the next birthday field on the entity.
DynamicEntity updateEntity = new DynamicEntity(EntityName.contact.ToString());
updateEntity["contactid"] = new Key(contactId);
updateEntity["new_nextbirthday"] = CrmDateTime.FromUniversal(nextBirthdate);

crmService.Update(updateEntity);

CompositeActivity parentActivity = this.Parent;
while (parentActivity.Parent != null)
{
parentActivity = parentActivity.Parent;
}

context.PopulateEntitiesFrom((CrmWorkflow)parentActivity, "primaryEntity");

//Allow the base class to continue the execution.
return ActivityExecutionStatus.Closed;
}

//Define the variables.
public static DependencyProperty ContactProperty = DependencyProperty.Register("Contact", typeof(Lookup), typeof(UpdateNextBirthday));

//Define the properties.
[CrmInput("Update Next Birthdate for")]
[ValidationOption(ValidationOption.Required)]
[CrmReferenceTarget("contact")]
public Lookup Contact
{
get
{
return (Lookup)base.GetValue(ContactProperty);
}
set
{
//Validate the argument.
if (value == null || (value.IsNullSpecified && value.IsNull))
{
throw new InvalidPluginExecutionException("Contact Lookup cannot be null or have IsNullSpecified = true");
}
else if (value.type != null && value.type != "contact")
{
throw new InvalidPluginExecutionException("Contact Lookup must be a contact entity");
}
else if (value.Value == Guid.Empty)
{
throw new InvalidPluginExecutionException("Contact Lookup must contain a valid Guid");
}

base.SetValue(ContactProperty, value);
}
}

private DateTime CalculateNextBirthday(DateTime birthdate)
{
DateTime nextBirthday = new DateTime(birthdate.Year, birthdate.Month, birthdate.Day);

//Check to see if this birthday occurred in a leap year.
bool leapYearAdjust = false;
if (nextBirthday.Month == 2 && nextBirthday.Day == 29)
{
//Verify that this year was a leap year.
if (DateTime.IsLeapYear(nextBirthday.Year))
{
//Check to see if the current year is a leap year.
if (!DateTime.IsLeapYear(DateTime.Now.Year))
{
//Push the date to March 1st so that the date arithmetic will function correctly.
nextBirthday = nextBirthday.AddDays(1);
leapYearAdjust = true;
}
}
else
{
throw new InvalidPluginExecutionException("Invalid Birthdate specified", new ArgumentException("Birthdate"));
}
}

//Calculate the year difference.
nextBirthday = nextBirthday.AddYears(DateTime.Now.Year - nextBirthday.Year);

//Check to see if the date was adjusted.
if (leapYearAdjust && DateTime.IsLeapYear(nextBirthday.Year))
{
nextBirthday = nextBirthday.AddDays(-1);
}

return nextBirthday;
}
}
}




Calculate Distance

The following sample uses the MapPoint service to calculate the distance between two zip codes using a simple route. It has two input parameters for the start and end zip codes and one output parameter for the total distance calculated.



using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.ComponentModel;
using System.Configuration;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.Workflow;
using System.Net;
using System.Xml;

namespace CustomWorkflowActivity
{
// Get more information about the mappoint assembly from http://staging.mappoint.net/standard-30/
using net.mappoint.staging;

[CrmWorkflowActivity("Calculate Distance", "Mappoint Utilities")]
public class DistanceCalculator : SequenceActivity
{
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
// Set the MapPoint ID and password you created with your Bing Developer/License Account.
ICredentials creds = new NetworkCredential("MappointID", "MappointPassword");
string DataSourceName = "MapPoint.NA";
FindServiceSoap findService = new FindServiceSoap();
findService.Credentials = creds;
findService.PreAuthenticate = true;

RouteServiceSoap routeService = new RouteServiceSoap();
routeService.Credentials = creds;
routeService.PreAuthenticate = true;
routeService.UserInfoRouteHeaderValue = new UserInfoRouteHeader();
routeService.UserInfoRouteHeaderValue.DefaultDistanceUnit = DistanceUnit.Mile;

FindAddressSpecification addressSpecStart = new FindAddressSpecification();
addressSpecStart.DataSourceName = DataSourceName;
addressSpecStart.InputAddress = new Address();
addressSpecStart.InputAddress.PostalCode = this.zipCodeStart;

FindAddressSpecification addressSpecEnd = new FindAddressSpecification();
addressSpecEnd.DataSourceName = DataSourceName;
addressSpecEnd.InputAddress = new Address();
addressSpecEnd.InputAddress.PostalCode = this.zipCodeEnd;

FindResults resultsStart = findService.FindAddress(addressSpecStart);
FindResults resultsEnd = findService.FindAddress(addressSpecEnd);

LatLong startLatLong = resultsStart.Results[0].FoundLocation.LatLong;
LatLong endLatLong = resultsEnd.Results[0].FoundLocation.LatLong;

RouteSpecification routeSpec = new RouteSpecification();
routeSpec.DataSourceName = DataSourceName;
routeSpec.Segments = new SegmentSpecification[2];

routeSpec.Segments[0] = new SegmentSpecification();
routeSpec.Segments[0].Waypoint = new Waypoint();
routeSpec.Segments[0].Waypoint.Location = resultsStart.Results[0].FoundLocation;

routeSpec.Segments[1] = new SegmentSpecification();
routeSpec.Segments[1].Waypoint = new Waypoint();
routeSpec.Segments[1].Waypoint.Location = resultsEnd.Results[0].FoundLocation;


Route route = routeService.CalculateSimpleRoute(new LatLong[] { startLatLong, endLatLong }, DataSourceName, SegmentPreference.Quickest);
this.totalDistance = new CrmNumber((int)route.Itinerary.Distance);

return ActivityExecutionStatus.Closed;
}

public static DependencyProperty zipCodeStartProperty = DependencyProperty.Register("zipCodeStart", typeof(string), typeof(DistanceCalculator));

[CrmInput("Starting Zip Code")]
public string zipCodeStart
{
get
{
return (string)base.GetValue(zipCodeStartProperty);
}
set
{
base.SetValue(zipCodeStartProperty, value);
}
}

public static DependencyProperty zipCodeEndProperty = DependencyProperty.Register("zipCodeEnd", typeof(string), typeof(DistanceCalculator));

[CrmInput("Ending Zip Code")]
public string zipCodeEnd
{
get
{
return (string)base.GetValue(zipCodeEndProperty);
}
set
{
base.SetValue(zipCodeEndProperty, value);
}
}

public static DependencyProperty totalDistanceProperty = DependencyProperty.Register("totalDistance", typeof(CrmNumber), typeof(DistanceCalculator));

[CrmOutput("Total Distance")]
public CrmNumber totalDistance
{
get
{
return (CrmNumber)base.GetValue(totalDistanceProperty);
}
set
{
base.SetValue(totalDistanceProperty, value);
}

}
}

}




Create a Task

The following sample workflow activity demonstrates how to create a task within an activity.



using System;
using System.Collections;
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 System.Reflection;

using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace SampleWorkflows
{
[CrmWorkflowActivity("Create a Task")]
public class CustomActivity : Activity
{
protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{

IContextService contextService = (IContextService)executionContext.GetService(typeof(IContextService));
IWorkflowContext context = contextService.Context;

ICrmService crmService = context.CreateCrmService();

DynamicEntity entity = new DynamicEntity();
entity.Name = EntityName.task.ToString();
entity.Properties = new PropertyCollection();
entity.Properties.Add(new StringProperty("subject", taskId.Value.ToString()));
entity.Properties.Add(new KeyProperty("activityid", new Key(taskId.Value)));
crmService.Create(entity);

return base.Execute(executionContext);
}

public static DependencyProperty taskIdProperty =
DependencyProperty.Register("taskId",
typeof(Lookup),
typeof(CustomActivity));

[CrmInput("The id")]
[CrmOutput("The output")]
[CrmReferenceTarget("task")]
public Lookup taskId
{
get
{
return (Lookup)base.GetValue(taskIdProperty);
}
set
{
base.SetValue(taskIdProperty, value);
}

}


}
}





Custom Workflow Activities for Microsoft Dynamics CRM 4.0

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.

Custom Workflow Activity to calculate and return

The following sample workflow activity demonstrates how to return a calculated value from an activity.



using System;
using System.Collections;
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 System.Reflection;

using Microsoft.Crm.Workflow;
using Microsoft.Crm.Sdk;
using Microsoft.Crm.SdkTypeProxy;
using Microsoft.Crm.Sdk.Query;

namespace SampleWorkflows
{
[CrmWorkflowActivity("Return a Calculated Value")]
public class AddActivity : Activity
{

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
result = new CrmNumber(a.Value + b.Value);
return base.Execute(executionContext);
}

public static DependencyProperty aProperty =
DependencyProperty.Register("a",
typeof(CrmNumber),
typeof(AddActivity));

[CrmInput("a")]
public CrmNumber a
{
get
{
return (CrmNumber)base.GetValue(aProperty);
}
set
{
base.SetValue(aProperty, value);
}

}

public static DependencyProperty bProperty =
DependencyProperty.Register("b",
typeof(CrmNumber),
typeof(AddActivity));

[CrmInput("b")]
public CrmNumber b
{
get
{
return (CrmNumber)base.GetValue(bProperty);
}
set
{
base.SetValue(bProperty, value);
}

}

public static DependencyProperty resultProperty =
DependencyProperty.Register("result",
typeof(CrmNumber),
typeof(AddActivity));

[CrmOutput("result")]
public CrmNumber result
{
get
{
return (CrmNumber)base.GetValue(resultProperty);
}
set
{
base.SetValue(resultProperty, value);
}

}
}
}