Sunday, December 14, 2008

How to Create a Custom ILM Workflow Activity - Part III: Creating the Custom Workflow Activity UI

This is the third part of a three part series on creating a custom workflow activity for ILM 2.0. A reminder that in order to create and build this solution you will need an implementation of the ILM 2.0 Beta 3  server or the RC Candidate with a copy of Visual Studio 2008 Professional Developer edition installed on it.

Sorry for taking so long to get this part out. I got stuck in a bit of the economic downturn we are all experiencing and fortunately, instead of having nothing to do, have spent the last 6 weeks building a software application for a customer and this is the first time I have come up for air.

If you want to download the source for this project you can find it here: ILM 2 Customer WF Activity on CodePlex.

I want to again mention my two colleagues Brad Turner and David Lundell for their support and contributions to this effort as well as ILM itself. Both are ILM MVPs and their blogs are worth exploring whenever you have questions about ILM.

In Part I, I laid out all the steps required to create the project and get it ready for developing the code. In Part II I we looked at how the project develops to the point where we have the ILM compatible workflow foundation activity defined and in Part III I will show you how to add the UI class that allows the activity to be listed in the available activities in ILM Process Designer. We'll also dig into how to install and configure the custom activity, add it to an existing Process workflow and see the results of our work.

Now let us create the class that will allow us to see our custom activity in the ILM Process Design Surface. Follow these steps to build the ILM Activity:

First let us create a new class named EnsynchDianosticActivityUI.cs. We also need to add the references we need to support ILM as well as the workflow engine and, since we will actually be building a .Net web control, we need to add the ASP.Net references as well.

using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Diagnostics;
using Microsoft.ResourceManagement.Utilities;
using Microsoft.IdentityManagement.WebUI;
using Microsoft.ResourceManagement.Workflow.Activities;
using Microsoft.IdentityManagement.WebUI.Controls;
using Microsoft.IdentityManagement.WebUI.Controls.Resources;



We are also going to change the namespace that I use for the interactive components to: EnSynch.ILM.Activities. This class needs to implement the "ActivitySettingsPart" interface so well add that to our class definition and allow it to flow out the implementation of our class for us (see below)



clip_image001



The resulting class structure for us to flesh out is shown below:



clip_image002



So again, we are going to add some helper functions that will help us create our user interface. These will create all the user interface components we need to show the user in order for them to provision this activity in the ILM Process Design Surface. Note: The EnsynchSwish.jpg file is included in the root folder of the solution in case you want to use it to see how it appears on the design surface. I created an Images folder in my SharePoint website root and am referencing the image file from there in the code below.



         #region Utility Functions

private void InitializeControls()
{
Table child = new Table();
child.Style.Add("background-image", "/Images/EnsynchSwish.jpg");
child.Width = Unit.Percentage(100.0);
child.BorderWidth = 0;
child.CellPadding = 2;
//
// We need an activity name for our instance, a folder path to where to write the file and a text box to hold our filename
// which we will default to myLog.txt
//
child.Rows.Add(AddTableRow("Activity Name:", "txtActivityName", 64, false, "Diagnostics"));
child.Rows.Add(AddTableRow("File Folder Path:", "txtFilePath", 256, false, "C:\\"));
child.Rows.Add(AddTableRow("
Diagnostic File Name:", "txtFileName", 64, false, "LogFile.txt"));

this.Controls.Add(child);
}

private TableRow AddTableRow(String labelText, String controlID, int maxLength, Boolean multiLine, string DefaultValue)
{
TableRow row = new TableRow();
TableCell labelCell = new TableCell();
TableCell controlCell = new TableCell();
Label olabel = new Label();
TextBox oText = new TextBox();
// Add the To:
olabel.Text = labelText;
olabel.CssClass = base.LabelCssClass;
labelCell.Controls.Add(olabel);
oText.ID = controlID;
oText.CssClass = base.TextBoxCssClass;
oText.Text = DefaultValue;
oText.MaxLength = maxLength;
if (multiLine)
{
oText.TextMode = TextBoxMode.MultiLine;
oText.Rows = System.Math.Min(6, (maxLength + 60) / 60);
}
controlCell.Controls.Add(oText);
row.Cells.Add(labelCell);
row.Cells.Add(controlCell);
return row;
}

private void SetControlAccess(string controlID, Boolean readOnly)
{
TextBox oText = (TextBox)this.FindControl(controlID);
if (oText != null)
oText.ReadOnly = readOnly;
}

private string GetControlString(string controlID)
{
TextBox oText = (TextBox)this.FindControl(controlID);
if (oText != null)
return oText.Text;
else
return "
[TextBox '" + controlID + "' Not Found";
}

private void SetTextBoxValue(string controlID, string value)
{
TextBox oText = (TextBox)this.FindControl(controlID);
if (oText != null)
oText.Text = value;
else
oText.Text = "
";
}

#endregion

#region Ensynch Event Log

private void SimpleLogFunction(string functionName, string Message, EventLogEntryType type, int eventID, short category)
{
using (StreamWriter mylog = new StreamWriter(@"
C:\MyErrorLog.txt", true))
{
mylog.WriteLine(DateTime.Now.ToString("
yyyy-MM-dd hh:mm:ss") + ", " + functionName + ", " + Message);
mylog.Close();
}
}

#endregion



We also need to add one more override method to our implementation, the CreateChildControls(), which will be called by the ILM interface to instruct our control to create its interface.



        protected override void CreateChildControls()
{
try
{
this.InitializeControls();
base.CreateChildControls();
}
catch (Exception ex)
{
SimpleLogFunction("Activitites.CreateChildControls", ex.Message, EventLogEntryType.Error, 1000, 100);
}
}



Notice I also have a call to my diagnostic SimpleLogFunction() here too. This version writes to "C:\MyErrorLog.txt" just in case I make a mistake somewhere.



NOTE: Notice my extensive use of try-catch-finally blocks in my code. While these are not always the most efficient pieces of code, we are writing both a user interface and a workflow component and we don't want them to fail in production or we at least need them to fail gracefully. If you do any extensive SharePoint development you will also want to take this tack as all the interfaces with SharePoint are web service based and with the data being so dynamic requests can fail anywhere so you will want to trap them.



So let's go through each interface method and implement code on them one by one.





















Interface Description
CreateChildControls This routine, as mentioned above places our controls into the design service for us. We build our utility functions to create our controls and place them within a table so we have a nicely laid out format.
GenerateActivityOnWorkflow This method will be called by ILM when the user hits the save button and ILM wants us to return an instance of our EnsynchDiagnosticActivity activity with the properties in it filled in with the values entered from the text boxes in our user interface. The code is shown below


        public override SequenceActivity GenerateActivityOnWorkflow(SequentialWorkflow workflow)
{
try
{
this.SimpleLogFunction("GenerateActivityOnWorkflow", "Starting Activity Generation", EventLogEntryType.Information, 10000, 100);
if (!this.ValidateInputs())
{
return null;
}
EnSynch.Workflow.Activities.EnsynchDiagnosticActivity activity =
new EnSynch.Workflow.Activities.EnsynchDiagnosticActivity();
this.SimpleLogFunction("GenerateActivityOnWorkflow", "Activity Created", EventLogEntryType.Information, 10000, 100);
activity.LogActivityName = GetControlString("txtActivityName");
activity.LogFolder = GetControlString("txtFilePath");
activity.LogFile = GetControlString("txtFileName");

this.SimpleLogFunction("GenerateActivityOnWorkflow", "Activity Properties Set", EventLogEntryType.Information, 10000, 100);

return activity;
}
catch (Exception ex)
{
SimpleLogFunction("GenerateActivityOnWorkflow Error", ex.Message, EventLogEntryType.Error, 1000, 100);
return null;
}
}



We create an instance of our activity, fill in the three properties we created (LogActivityName, LogFolder, and LogFile) by using a helper function, GetControlString() and then simply return it.



The next several sections cover each interface in detail:















Interface Description
LoadActivitySettings This is called by ILM when it wants us to reload our user interface. It passes us a definition of our activity from its XAML definition. The code for LoadActivitySettings() is shown below


        public override void LoadActivitySettings(SequenceActivity activity)
{
EnSynch.Workflow.Activities.EnsynchDiagnosticActivity activity2 =
activity as EnSynch.Workflow.Activities.EnsynchDiagnosticActivity;
if (activity2 != null)
{
SetTextBoxValue("txtActivityName", activity2.IsDynamicActivity);
SetTextBoxValue("txtFilePath", activity2.LogFolder);
SetTextBoxValue("txtFileName", activity2.LogFile);
}
}















Interface Description
PersistSettings Packages up the activity properties in an ActivitySettingsPartData collection and returns them to ILM for processing


         public override ActivitySettingsPartData PersistSettings()
{
ActivitySettingsPartData data = new ActivitySettingsPartData();
data["LogActivityName"] = GetControlString("txtActivityName");
data["LogFolder"] = GetControlString("txtFilePath");
data["LogFile"] = GetControlString("txtFileName");
return data;
}















Interface Description
RestoreSetting Allows our interface to restore the values of our properties to the interface


        public override void RestoreSettings(ActivitySettingsPartData data)
{
if (data != null)
{
SetTextBoxValue("txtActivityName", data["LogActivityName"] as string);
SetTextBoxValue("txtFilePath", data["LogFolder"] as string);
SetTextBoxValue("txtFileName", data["LogFile"] as string);
}
}















Interface Description
SwitchMode This routine is called by ILM when it wants our control to switch from edit to view modes. Again, we make user of a helper function SetControlAccess() to set the access to read only in view mode.


        public override void SwitchMode(ActivitySettingsPartMode mode)
{
bool flag = mode != ActivitySettingsPartMode.Edit;
SetControlAccess("txtActivityName", flag);
SetControlAccess("txtFilePath", flag);
SetControlAccess("txtFileName", flag);
}















Interface Description
Title The title property provides the ILM interface with the title string that it will display for us on the design surface.


         public override string Title
{
get
{
return "Ensynch Diagnostic Activity";
}
}















Interface Description
ValidateInputs ILM calls this method as the first step when a user presses the Save button on our activity on the design surface. In our case we are going to assume that the user always puts valid file and folder names. By the way, remember that since the target folder and file are going to be on the server any validation we do would be against that machine and not necessarily the client we are working on.


So we are going to simply return "true" from this routine.



        public override bool ValidateInputs()
{
return true;
}



So that's it. We have completed the coding for our activity. We can now build our library.



Building the library and loading it so that ILM will see it


So for ILM to make use of our custom activity, at least in Beta 3, the library will need to be signed and placed in the Global Assembly Cache (GAC) as well the _app_bin folder of the SharePoint application the ILM portal is installed into.



To install your library into the GAC you can simply copy the library into the C:\WINDOWS\assembly folder or you can use gacutil.exe to do that for you. See this reference: http://support.microsoft.com/kb/315682 for complete instructions on signing and installing a library into the GAC.



In my case I am adding my control to an existing library that was already been created and signed. The illustration below shows the property sheet for the signing page of my library.



clip_image003



The first time you install your library in the GAC you will want to grab the public key token and version information. We will need that in the next step. From the C:\Windows\assembly right click on your library and select Properties.



clip_image004



clip_image005



So we are going to need the public key token: "6339eb144475917c" and the version: "1.0.0.0" for the next step.



Exposing our Activity to the ILM Process Designer


We are almost done. We need to copy our library to the _app_bin folder of our SharePoint web site. In my case this is in "C:\inetpub\wwwroot\wss\VirtualDirectories\80\_app_bin". I am going to copy two files to this folder; my DLL and also the PDB file for my library which will allow me to attach to the SharePoint executable (w3wp.exe) to debug my code in the event there are some bugs to check out. When I get done my _app_bin folder will look like:



clip_image006



Notice this includes all of the ILM libraries as well.



After installing our libraries we need to reset IIS and restart the ILM services in order for them to connect with our latest version of the library we just deployed. You can open a Command window on the SharePoint server and enter the: "iisreset" command and hit return. This will reset IIS. Next open the Services application from the Administrative Tools folder.



In the Services application you will want to restart the ILM services. In the illustration below they are the first two services in the listing "Microsoft Identity Integration Server" and "Microsoft ILM Common Services".



clip_image007



Figure 1‑20 - Services, ILM Services



NOTE: Every time you rebuild your library you will need to copy the new version to the GAC and to the _app_bin folder.



One more step to go. We have to tell ILM about our custom activity. To do this we need to add our activity definition to the configuration file ILM uses for its Activity definitions:



Microsoft.IdentityManagement.Activities.arp



The copy of “Microsoft.IdentityManagement.Activities.arp” we want to use is also located in the _app_bin folder of our SharePoint web site. There is a second copy located in the ILM folders in “Program Files\Microsoft Identity Management” which is not actually used by the portal.[1] Again, in my case this is in:



"C:\inetpub\wwwroot\wss\VirtualDirectories\80\_app_bin"



The configuration entry for our new activity is shown below:



<IdentityManagementActivity xmlns="http://schemas.microsoft.com/IdentityManagement/v1">
<Assembly>EnsynchILMActivity, Version=1.0.0.0, Culture=neutral, PublicKeyToken=6339eb144475917c</Assembly>
<TypeName>EnSynch.ILM.Activities.EnsynchDiagnosticActivityUI</TypeName>
<ActivityName>EnSynch.Workflow.Activities.EnsynchDiagnosticActivity</ActivityName>
<Title>Ensynch Diagnostic Activity</Title>
<Description>Ensynch ILM 2 Activity that writes Request data to a specified log file</Description>
<PartImageSmall>/_layouts/images/approval_small.gif</PartImageSmall>
<PartImageLarge>/_layouts/images/approval_large.gif</PartImageLarge>
<Categories>
<Category>Authentication</Category>
<Category>Authorization</Category>
<Category>Action</Category>
</Categories>
</IdentityManagementActivity>


The definition of all the fields is shown in the table below:































































Element Name Description
<Assembly> </Assembly>

This is the date associated with the assembly that contains the custom activity. The contents should contain:



Assembly name, Version, Culture, and the Public Key Token.



Assembly Name: The assembly name is the name of the DLL minus the extension. In the example able the assembly DLL is named:



Ensynch.IdentityManagement.Activities.dll.



Version: The DLL version. In this example we are using version 1.0.0.0



Culture: The culture the assembly was created for. In this case the assembly is neutral.



PublicKeyToken: If you are putting your assembly into the GAC you will need to strongly type your assembly and copy it into the GAC. You can look at the properties once you have installed it there and access the public key token and the version.


<TypeName> </TypeName> The full class name (including the namespace) of the class that provides the visual component in the Process Designer
<ActivityName> </ActivityName> The full class name (including the namespace) of the workflow activity class that is associated with visual component.
<Title></Title> The title as you want it to appear in the designer’s activity list
<Description> </Description> The title as you want it to appear in the designer’s activity list
<PartImageSmall> </PartImageSmall> The URL to the small icon image file you want associated with your activity
<PartImageLarge> </PartImageLarge> The URL to the large icon image file you want associated with your activity
<Categories> </Categories> This the list of Process categories you designed or want your activity to appear in. Valid categories include: Authentication, Authorization and Action
<Category> </Category> Element for each action to include. Must be a child of <Caterories> element.


So now let's bring up the portal and test out our new activity. In my example I am trying to monitor the progress of the workflow properties through the "AD Outbound Sync Process", one of the processes that supports synchronizing various custom activities for an organization.



In the Activities Tab of the Process editor we can see the process as it exists right now:



clip_image008



I am going to select the "Add Activity" option on the bottom which brings up a list of activities for me to select from:



clip_image009



My activity is the one on the bottom. I select it and press Select. This brings up a copy of my activity in edit mode. Notice the three fields for setting our properties.



clip_image010



I'm going to take the default folder and file name but I'm going to change the activity name to "After Login Name" and then save it and move it up just after the first activity.



clip_image011



I'm going to do this after one more time and locate the second activity after the Password activity. My end result will look like this:



clip_image012



So this is nice but we can make a minor enhancement to our activity that will make it even better.



I'm going to change my Title property so that it includes the Activity Name we type in. My new code will look like this:



public override string Title
{
get
{
return "Ensynch Diagnostic Activity: " + GetControlString("txtActivityName");
}
}



This makes my activity list look as shown below which makes the activity easier to understand.



clip_image013



We press OK and then Submit and we are done with this step.



Testing out our Activity in Practice


Now we need to see if our activity works. Let's add a new user and see if we can follow the progress of the workflow in our log file.



In the ILM Portal select "All Users" from the Quick Launch Bar and go to the "All Users" page.



clip_image014



Pick the "New" user icon and create a new user. I am creating a full time employee named Adam Diagnostics as my test user. The next two shots show the data that I entered for Adam.



clip_image015



Figure 1‑28 - ILM Portal, Create User - Adam Diagnostic



clip_image016



Since I am done entering Adam's information I select the Finish button and then Submit from the summary screen.



Adam gets added to the list of users. The synchronization workflow will kick off at this point and run through all the activities in our process.



When we look at the log file we created from our custom activity we see that it has recorded the information we were interested in:



Note: As of this writing we now know that the ARP file will be replaced in the release candidate and will be stored in the ILM database.



clip_image017



So that's it. You have now created a custom activity for an ILM workflow.

Monday, October 20, 2008

How to Create a Custom ILM Workflow Activity - Part II: Creating the Custom Workflow Activity

This is the second part of a three part series on creating a custom workflow activity for ILM 2.0. A reminder that in order to create and build this solution you will need an implementation of the ILM 2.0 Beta 3 server with a copy of Visual Studio 2008 Professional Developer edition installed on it. You can read the first entry here: How to Create a Custom ILM 2.0 Beta 3 Workflow Activity - Part I

I want to again mention my two colleagues Brad Turner and David Lundell for their support and contributions to this effort as well as ILM itself. Both are ILM MVPs and their blogs are worth exploring whenever you have questions about ILM.

In Part I, I laid out all the steps required to create the project and get it ready for developing the code. In Part II I will show you how the project develops to the point where we have the ILM compatible workflow foundation activity defined and in Part III I will show you how to add the UI class that allows the activity to be listed in the available activities in ILM Process Designer.

Picking up from where we left off, create a new class file named:

EnsynchDiagnosticActivity.cs

Use the Activity template in the Workflow category as shown below.

clip_image001

NOTE: You will probably have to add the ILM custom activities to your toolbox so right click on the toolbox and select "Add Tab" and add a new tab named "ILM Activities".

clip_image002

You will end up with an empty new category:

clip_image003

Right click in the ILM Activities space and select the "Choose Items …" option.

Select the "Browse…" button and navigate to the _app_bin folder of your SharePoint web site and select the Microsoft.ResourceManagement.dll library.

clip_image004

Press Open. If you sort the resulting list by Namespace you will see all the activities highlighted for this library.

clip_image005

Figure 1‑7 - Visual Studio, Toolbox Items

Press OK and the activities will be added to your ILM Activities Toolbox.

clip_image006

Drag a CurrentRequestActivity onto the design surface and rename the activity "LogRequestActivity". Your activity should look like it does below:

clip_image007

Figure 1‑9 - Visual Studio, WF Designer

Right click on the design surface and select View Code

In the code we need to add the references to the ILM object model and also a reference to the collections object mode which many of the dictionary items are built from. And since we are potentially going to be working with some diagnostics level bits we add that along with System.IO since we’re going to be writing to a file.

using System.Diagnostics;
using System.Collections.Generic;
using Microsoft.IdentityManagement.WebUI;
using Microsoft.ResourceManagement.Workflow.Activities;
using Microsoft.ResourceManagement.WebServices.WSResourceManagement;
using Microsoft.IdentityManagement.WebUI.Controls;
using Microsoft.ResourceManagement.Utilities;
using System.Collections.ObjectModel;
using System.IO;



Now we need to add a variable that will hold the data associated with the ILM request type. We'll call that currentRequest. For organizational purposes I like to isolate different sections of my code into regions. We'll locate this code in the "Activity Properties" region.



public RequestType currentRequest;



Now, because we are building a diagnostic activity we will want to write the diagnostics to a file in a folder of our choice. In addition we will want to provide a name that that will allow us to pinpoint where we have placed a copy of this activity in our workflow. So we need three additional properties that we will create in the user interface component of this activity. We create these as dependency properties, which will allow the ILM design surface to set the properties of the activity and have them persist into the workflow definition. These properties actually show up as attributes of the activity definition in the XOML workflow definition we will create in the ILM portal using our activity.



So we create three properties, LogActivityName, LogFolder and LogFile:



        public static DependencyProperty LogActivityNameProperty =
DependencyProperty.Register("LogActivityName", typeof(string), typeof(EnsynchDiagnosticActivity));

[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Properties")]
public string LogActivityName
{
get { return (string)this.GetValue(EnsynchDiagnosticActivity.LogActivityNameProperty); }
set
{
this.SetValue(EnsynchDiagnosticActivity.LogActivityNameProperty, value);
}
}

public static DependencyProperty LogFolderProperty =
DependencyProperty.Register("LogFolder", typeof(string), typeof(EnsynchDiagnosticActivity));

[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Properties")]
public string LogFolder
{
get { return (string)this.GetValue(EnsynchDiagnosticActivity.LogFolderProperty); }
set
{
this.SetValue(EnsynchDiagnosticActivity.LogFolderProperty, value);
}
}

public static DependencyProperty LogFileProperty =
DependencyProperty.Register("LogFile", typeof(string), typeof(EnsynchDiagnosticActivity));

[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Properties")]
public string LogFile
{
get { return (string)this.GetValue(EnsynchDiagnosticActivity.LogFileProperty); }
set
{
this.SetValue(EnsynchDiagnosticActivity.LogFileProperty, value);
}
}



Now let's go back to the design surface. We need to connect the CurrentRequestActivity we placed into our activity to the currentRequest parameter we created.



clip_image008



Pick the ellipses to the right of the CurrentRequest property.



clip_image009



Select the currentRequest (this is the variable we created in the code) and select OK. If you open up the CurrentRequest property now it will show how it is connected to our code. A little side note here: we could have selected the “Bind to a new member” tab and created the “currentRequest” parameter right from here instead of manually coding it in as we did above.



clip_image010



Save this and then go back to the code. Let's take a quick look at the SequenceActivity that our activity is built from. Right click on the SequenceActivity interface and select "Go To Definition"



clip_image011



Figure 1‑13 –Selecting the “Go To Definition” option on the SequenceActivity class



NOTE: You should see the following definition:



clip_image012



Figure 1‑14 –Summary view of the SequenceActivity Class Definition



For our purposes we are going to implement an override of the OnSequenceComplete event handler and the HandleFault event handler in case we run into a problem.



So now back to our code. I'm going to paste in the code I am going use for these two events which looks like what you are seeing below. In the “OnSequenceComplete” method I am writing out the object type and the request type as well as cycling through the request parameters and workflows dictionary to write those out as well. For the object type and request type I am using a couple of helper functions that we’ll insert a little further down.



#region Event Processing

protected override void OnSequenceComplete(ActivityExecutionContext executionContext)
{
try
{
SequentialWorkflow containingWorkflow = null;
if (!SequentialWorkflow.TryGetContainingWorkflow(this, out containingWorkflow))
{
throw new InvalidOperationException("Unable to get Containing Workflow");
}
//
// Output the Request type and object type
//
this.SimpleLogFunction("Request Object Type = \"" + this.GetObjectType() + "\"", "", EventLogEntryType.Information, 10002, 100);
this.SimpleLogFunction("Request Operation Type = \"" + this.GetOperation() + "\"", "", EventLogEntryType.Information, 10002, 100);
//
// UpdateRequestParameter derives from CreateRequestParameter. Since we only need PropertyName / value pairs,
// simplify the code to work on CreateRequestParameter only.
//
ReadOnlyCollection<CreateRequestParameter> requestParameters = this.currentRequest.ParseParameters<CreateRequestParameter>();
//
// Traverse CreateRequestParameters in and print out each attribute in the request
//
this.SimpleLogFunction("Cycle Through the Request Parameters", "", EventLogEntryType.Information, 10002, 100);

foreach (CreateRequestParameter requestParameter in requestParameters)
{
this.SimpleLogFunction("RequestParameter (\"" + requestParameter.PropertyName + "\")",
" = \"" + requestParameter.Value.ToString() + "\"", EventLogEntryType.Information, 10002, 100);
}

this.SimpleLogFunction("Cycle Through the Workflow Dictionary", "", EventLogEntryType.Information, 10002, 100);

foreach (KeyValuePair<string, object> dItem in containingWorkflow.WorkflowDictionary)
{
this.SimpleLogFunction("Dictionary Entry (\"" + dItem.Key + "\")",
" = \"" + dItem.Value.ToString() + "\"", EventLogEntryType.Information, 10002, 100);
}
}
catch (Exception ex)
{
this.SimpleLogFunction("Diagnostic Log Entry Request Exception", " = '" + ex.Message + "'", EventLogEntryType.Error, 10005, 100);
}
finally
{
base.OnSequenceComplete(executionContext);
}
}
protected override ActivityExecutionStatus HandleFault(ActivityExecutionContext executionContext, Exception exception)
{
SimpleLogFunction("HandleFault", exception.Message, EventLogEntryType.Error, 1000, 100);

return ActivityExecutionStatus.Closed;
}
#endregion



Notice I keep calling a function named SimpleLogFunction. So we need to add one additional section, the Utility Function region that holds that routine. Here I simply (hence the name) create/open the file in the folder specified and append a record containing the specified entry to the file. This is also where we add our two helper functions to output the request type and object type.



#region Utility Functions

private void SimpleLogFunction(string functionName, string Message, EventLogEntryType type, int eventID, short category)
{
string delim = "";

using (StreamWriter mylog = new StreamWriter(Path.Combine(this.LogFolder, this.LogFile), true))
{
mylog.WriteLine(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss") + ": " +
this.LogActivityName + ": " +
functionName + ", " + Message);
mylog.Close();
}
}

private string GetOperation()
{
switch (this.currentRequest.Operation)
{
case OperationType.Create:
return "Create";
case OperationType.Delete:
return "Delete";
case OperationType.Enumerate:
return "Enumerate";
case OperationType.Get:
return "Get";
case OperationType.Pull:
return "Pull";
case OperationType.Put:
return "Put";
case OperationType.SystemEvent:
return "System Event";
default:
return "Unknown Operation";
}
}

private string GetObjectType()
{
return currentRequest.TargetObjectType;
}

#endregion



So that's it for creating the workflow activity itself. Let's stop and build this solution just to make sure we did everything correctly.



In the third and last post in this series, we'll create the ILM UI class that pulls the whole solution together and allows us to end up with a custom workflow activity that we can see in the ILM Process Manager Design Surface. Stay Tuned.

Wednesday, October 15, 2008

How to Create a Custom ILM 2.0 Beta 3 Workflow Activity - Part I

As my colleagues and I at Ensynch, specifically Brad Turner and David Lundell, were digging into ILM 2.0 Beta 3, the power of the new Processing features that are built into this new version took us on a wonder journey that started generating ideas about how we could use this to facilitate all sorts of cool add-ons that you would potentially want to have happen when a part or all of a person's identity changed.

I mean, when a new employee was added could we add a custom activity to the ILM library of activities that would calculate a unique user name? Could we create an activity that would allow us to easily integrate with an external system and be easily configured to access that system and return some key data value or values that we could add to the request stream. Could we then make use of that data in the action step to fill in extended properties in the target object of the request that represented that external system?

We thought, "we have to learn how to make this work". So, in my first set of blogs, ever, I am going to layout the basic steps to create a custom ILM workflow activity. I'm going to lay this out from the perspective of an Infrastructure Engineer who has done some dabbling into code with Visual Studio but who does not make a career out of developing but would rather spend their time coordinating and configuring ILM for their customers and companies.

What's nice about this activity is that it posts diagnostic records out to a log file, of your own choosing, which prints out all the parameters of the workflow, the request object and the target object. You can insert this activity before and after any activity in your ILM Process in order to track what is happening to these parameters as your workflow runs. This can help ILM administrators create and debug Process Flows without the need to writing custom code to analyze problems they might encounter along the way.

In fact we don't actually get to the code until second post so bear with me and all will be revealed over the next couple of weeks.

To create the custom activity we have designed here you will need an implementation of the ILM 2.0 Beta 3 server with a copy of Visual Studio 2008 Professional Developer edition installed. You will be working directly on the SharePoint server that the ILM portal has been installed on. In Beta 3.0 the installations have all the ILM components installed on the same server or virtual machine. For the Beta 3 release the library we are going to create will have to be installed both in the Global Assembly Cache (GAC) as well as the _app_bin folder of the ILM SharePoint Portal. These instructions may change for the Release Candidate (RC) but for now the need to copy the library to both locations is a requirement.

By the way I will be creating this example in C# but the code is easily translatable unto VB.

Creating the Visual Studio Project

The first step in this process is to create a Workflow Activity library as shown in the figure below.

clip_image001

Figure 1‑1 - Creating WF Project

Once the project is opened we want to preset a few things. We want to set the default namespace to something more meaningful than out project name. In my case I am going to be using two namespaces, one for the workflow activity (EnSynch.Workflow.Activities) and another for the user interface component of the activity (EnSynch.ILM.Activities).

In the Solutions Explorer Right click on the EnsynchCustomActivities project and select "Properties"

clip_image002

Figure 1‑2 - Visual Studio, changing default namespace

Change the name of the "Default namespace" field to "Ensynch.Workflow.Activities". Save this configuration and close the property sheet;

Now select the Signing tab on the left side. Since we are going to have to place our library into the Global Assembly Cache (GAC) we need to have a signed library.

clip_image003

Check the "Sign the assembly" checkbox and select the <New…> option from the dropdown list.

clip_image004

In the Create Strong Name Key dialog box enter a name for your key file (in my case I entered Ensynch.snk) uncheck the "Protect my key file with a password" checkbox and press OK.

clip_image005

Do a Save All on your project and close the properties window.

We need to add the ILM libraries as references to our project. In the solutions explorer, right click on the References folder and select “Add Reference”

clip_image006

Select the Browse tab.

Browse to the _app_bin folder of your SharePoint. Select all the ILM libraries as shown in the figure below. Press OK

clip_image007

Do a Save All on your project.

You can delete the Activity1.cs file.

So that's it for this entry in Part II I'll show you how to create the custom workflow activity and then in Part III we'll create the ILM user interface class and install and configure the completed assembly up to ILM.