Here’s how I got the Custom Workflow Logging example working in VB.NET. Please consult this post together with the Microsoft document as I’m not going to reproduce the entire thing here. The usual warnings about me being no great developer also apply.Â
Project Naming
If you want to use this as a starting point to writing more workflow activities then you should start by following the document How to: Create a Custom Activity Library, making the following changes for VB.NET:Â
1. Obviously, wherever it says “Visual C#” or “.vc” just using the VB.NET alternatives.Â
2. The “Application” tab is different for VB.NET:Â
- For the Assembly Name I ended up with “FIM.CustomWorkflowActivitiesLibrary”
- There is no “Default namespace” setting that I can find. Instead I set Root namespace to “FIM.CustomWorkflowActivitiesLibrary”.
- You will find the Target Framework setting under Compile -> Advanced Compile Options.
For reference, my solution ends up looking like this:Â
Â
and the Object Browser shows these namespaces and classes:Â
Â
Build Settings
The walkthrough tells you how to add post-build steps so you don’t need to go through the process of putting your dll in the GAC and restarting FIMÂ each time. Again the location for the modificaion is different for VB.NET. So you need to open the Solution Properties and then go to Compile -> Build Events. Paste the following lines in to the “Post-build event command line” box:
Â
"C:\Program Files\Microsoft SDKs\Windows\v6.0A\Bin\gacutil.exe" /i "$(TargetPath)"
net stop "Forefront Identity Manager Service"
net start "Forefront Identity Manager Service"
Adding FIM Out-of-Box Activities to the Toolbox
This section is basically the same for both VC# and VB.NET.Â
Note, however, that the doc says to clear all the existing workflow actions, but then later you need the Code action to still be in the Toolbox. I wouldn’t bother clearing the existing actions – just add your FIM ones.Â
Defining the activity sequence and properties
Here’s the RequestLoggingActivity code in VB.NET.Â
Imports System.Collections.Generic Imports System.Collections.ObjectModel Imports System.IO Imports Microsoft.ResourceManagement.WebServices.WSResourceManagement Imports Microsoft.ResourceManagement.Workflow.Activities Public Class RequestLoggingActivity Inherits SequenceActivity #Region "Public Workflow Properties" Public Shared ReadCurrentRequestActivity_CurrentRequestProperty As DependencyProperty = DependencyProperty.Register("ReadCurrentRequestActivity_CurrentRequest", GetType(Microsoft.ResourceManagement.WebServices.WSResourceManagement.RequestType), GetType(RequestLoggingActivity)) ''' <summary> ''' Stores information about the current request ''' </summary> <DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)> _ <BrowsableAttribute(True)> _ <CategoryAttribute("Misc")> _ Public Property ReadCurrentRequestActivity_CurrentRequest() As RequestType Get Return DirectCast(MyBase.GetValue(ReadCurrentRequestActivity_CurrentRequestProperty), Microsoft.ResourceManagement.WebServices.WSResourceManagement.RequestType) End Get Set(ByVal value As RequestType) MyBase.SetValue(RequestLoggingActivity.ReadCurrentRequestActivity_CurrentRequestProperty, value) End Set End Property ''' <summary> ''' Identifies the Log File Path ''' </summary> Public Shared LogFilePathProperty As DependencyProperty = DependencyProperty.Register("LogFilePath", GetType(System.String), GetType(RequestLoggingActivity)) <Description("Please specify the Log File Path")> _ <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)> _ <Browsable(True)> _ Public Property LogFilePath() As String Get Return DirectCast(MyBase.GetValue(RequestLoggingActivity.LogFilePathProperty), [String]) End Get Set(ByVal value As String) MyBase.SetValue(RequestLoggingActivity.LogFilePathProperty, value) End Set End Property ''' <summary> ''' Identifies the Log File Name ''' </summary> Public Shared LogFileNameProperty As DependencyProperty = DependencyProperty.Register("LogFileName", GetType(System.String), GetType(RequestLoggingActivity)) <Description("Please specify the Log File Path")> _ <DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)> _ <Browsable(True)> _ Public Property LogFileName() As String Get Return DirectCast(MyBase.GetValue(RequestLoggingActivity.LogFileNameProperty), [String]) End Get Set(ByVal value As String) MyBase.SetValue(RequestLoggingActivity.LogFileNameProperty, value) End Set End Property #End Region #Region "Execution Logic" ''' <summary> ''' Defines to logic of the LogRequestDataToFile activity. ''' This code will be executed when the LogRequestDataToFile activity ''' becomes to active workflow. ''' </summary> ''' <param name="sender"></param> ''' <param name="e"></param> ''' <remarks></remarks> Private Sub LogRequestDataToFile_ExecuteCode(ByVal sender As System.Object, ByVal e As System.EventArgs) 'Get current request from previous activity Dim currentRequest As RequestType = Me.ReadCurrentRequestActivity_CurrentRequest Try ' Output the Request type and object type Me.Log("Request Operation: " & currentRequest.Operation) Me.Log("Target Object Type: " & currentRequest.TargetObjectType) ' As UpdateRequestParameter derives from CreateRequestParameter we can simplify the code by deriving ' from CreateRequestParameter only. Dim requestParameters As ReadOnlyCollection(Of CreateRequestParameter) = currentRequest.ParseParameters(Of CreateRequestParameter)() ' Loop through CreateRequestParameters and print out each attribute/value pair Me.Log("Parameters for request: " + currentRequest.ObjectID.ToString) For Each requestParameter As CreateRequestParameter In requestParameters If requestParameter.Value IsNot Nothing Then Me.Log((" " + requestParameter.PropertyName & ": ") & requestParameter.Value.ToString()) End If Next Dim containingWorkflow As SequentialWorkflow = Nothing ' In order to read the Workflow Dictionary we need to get the containing (parent) workflow If Not SequentialWorkflow.TryGetContainingWorkflow(Me, containingWorkflow) Then Throw New InvalidOperationException("Unable to get Containing Workflow") End If Me.Log("Containing Workflow Dictionary (WorkflowData):") ' Loop through Workflow Dictionary and log each attribute/value pair For Each item As KeyValuePair(Of String, Object) In containingWorkflow.WorkflowDictionary Me.Log((" " & item.Key & ": ") & item.Value.ToString()) Next Me.Log(vbLf & vbLf) Catch ex As Exception Me.Log("Logging Activity Exception Thrown: " & ex.Message) End Try End Sub #End Region #Region "Utility Functions" ' Prefix the current time to the message and log the message to the log file. Private Sub Log(ByVal message As String) Using log As New StreamWriter(Path.Combine(Me.LogFilePath, Me.LogFileName), True) 'since the previous line is part of a "using" block, the file will automatically 'be closed (even if writing to the file caused an exception to be thrown). 'For more information see ' http://msdn.microsoft.com/en-us/library/yh598w02.aspx log.WriteLine(DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss") + ": " & message) End Using End Sub #End Region End Class
 Â
Creating a User Interface for the Activity
Here’s the code for this section in VB.NET. Note that I have changed the Namespace definition to get round the VB.NET “Root namespace” setting working differently to the VC# “Default namespace” setting.Â
 Â
Imports System Imports System.Collections.Generic Imports System.Linq Imports System.Text Imports System.Web.UI.WebControls Imports System.Workflow.ComponentModel Imports Microsoft.IdentityManagement.WebUI.Controls Imports Microsoft.ResourceManagement.Workflow.Activities Imports FIM.CustomWorkflowActivitiesLibrary.Activities Public Class RequestLoggingActivitySettingsPart Inherits ActivitySettingsPart ''' <summary> ''' Called when a user clicks the Save button in the Workflow Designer. ''' Returns an instance of the RequestLoggingActivity class that ''' has its properties set to the values entered into the text box controls ''' used in the UI of the activity. ''' </summary> Public Overrides Function GenerateActivityOnWorkflow(ByVal workflow As SequentialWorkflow) As Activity If Not Me.ValidateInputs() Then Return Nothing End If Dim LoggingActivity As New RequestLoggingActivity() LoggingActivity.LogFilePath = Me.GetText("txtLogFilePath") LoggingActivity.LogFileName = Me.GetText("txtLogFileName") Return LoggingActivity End Function ''' <summary> ''' Called when editing the workflow activity settings. ''' </summary> Public Overrides Sub LoadActivitySettings(ByVal activity As Activity) Dim LoggingActivity As RequestLoggingActivity = TryCast(activity, RequestLoggingActivity) If LoggingActivity IsNot Nothing Then Me.SetText("txtLogFilePath", LoggingActivity.LogFilePath) Me.SetText("txtLogFileName", LoggingActivity.LogFileName) End If End Sub ''' <summary> ''' Saves the activity settings. ''' </summary> Public Overrides Function PersistSettings() As ActivitySettingsPartData Dim data As New ActivitySettingsPartData() data("LogFilePath") = Me.GetText("txtLogFilePath") data("LogFileName") = Me.GetText("txtLogFileName") Return data End Function ''' <summary> ''' Restores the activity settings in the UI ''' </summary> Public Overrides Sub RestoreSettings(ByVal data As ActivitySettingsPartData) If data IsNot Nothing Then Me.SetText("txtLogFilePath", DirectCast(data("LogFilePath"), String)) Me.SetText("txtLogFileName", DirectCast(data("LogFileName"), String)) End If End Sub ''' <summary> ''' Switches the activity between read only and read/write mode ''' </summary> Public Overrides Sub SwitchMode(ByVal mode As ActivitySettingsPartMode) Dim [readOnly] As Boolean = (mode = ActivitySettingsPartMode.View) Me.SetTextBoxReadOnlyOption("txtLogFilePath", [readOnly]) Me.SetTextBoxReadOnlyOption("txtLogFileName", [readOnly]) End Sub ''' <summary> ''' Returns the activity name. ''' </summary> Public Overrides ReadOnly Property Title() As String Get Return "Request Logging Activity" End Get End Property ''' <summary> ''' In general, this method should be used to validate information entered ''' by the user when the activity is added to a workflow in the Workflow ''' Designer. ''' We could add code to verify that the log file path already exists on ''' the server that is hosting the FIM Portal and check that the activity ''' has permission to write to that location. However, the code ''' would only check if the log file path exists when the ''' activity is added to a workflow in the workflow designer. This class ''' will not be used when the activity is actually run. ''' For this activity we will just return true. ''' </summary> Public Overrides Function ValidateInputs() As Boolean Return True End Function ''' <summary> ''' Creates a Table that contains the controls used by the activity UI ''' in the Workflow Designer of the FIM portal. Adds that Table to the ''' collection of Controls that defines each activity that can be selected ''' in the Workflow Designer of the FIM Portal. Calls the base class of ''' ActivitySettingsPart to render the controls in the UI. ''' </summary> Protected Overrides Sub CreateChildControls() Dim controlLayoutTable As Table controlLayoutTable = New Table() 'Width is set to 100% of the control size controlLayoutTable.Width = Unit.Percentage(100.0) controlLayoutTable.BorderWidth = 0 controlLayoutTable.CellPadding = 2 'Add a TableRow for each textbox in the UI controlLayoutTable.Rows.Add(Me.AddTableRowTextBox("Log File Path:", "txtLogFilePath", 400, 100, False, "Enter the log file Path.")) controlLayoutTable.Rows.Add(Me.AddTableRowTextBox("Log File Name:", "txtLogFileName", 400, 100, False, "Enter the log file Name.")) Me.Controls.Add(controlLayoutTable) MyBase.CreateChildControls() End Sub #Region "Utility Functions" 'Create a TableRow that contains a label and a textbox. Private Function AddTableRowTextBox(ByVal labelText As [String], ByVal controlID As [String], ByVal width As Integer, ByVal maxLength As Integer, ByVal multiLine As [Boolean], ByVal defaultValue As [String]) As TableRow Dim row As New TableRow() Dim labelCell As New TableCell() Dim controlCell As New TableCell() Dim oLabel As New Label() Dim oText As New TextBox() oLabel.Text = labelText oLabel.CssClass = MyBase.LabelCssClass labelCell.Controls.Add(oLabel) oText.ID = controlID oText.CssClass = MyBase.TextBoxCssClass oText.Text = defaultValue oText.MaxLength = maxLength oText.Width = width If multiLine Then oText.TextMode = TextBoxMode.MultiLine oText.Rows = System.Math.Min(6, (maxLength + 60) \ 60) oText.Wrap = True End If controlCell.Controls.Add(oText) row.Cells.Add(labelCell) row.Cells.Add(controlCell) Return row End Function Private Function GetText(ByVal textBoxID As String) As String Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox) Return If(textBox.Text, [String].Empty) End Function Private Sub SetText(ByVal textBoxID As String, ByVal text As String) Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox) If textBox IsNot Nothing Then textBox.Text = text Else textBox.Text = "" End If End Sub 'Set the text box to read mode or read/write mode Private Sub SetTextBoxReadOnlyOption(ByVal textBoxID As String, ByVal [readOnly] As Boolean) Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox) textBox.[ReadOnly] = [readOnly] End Sub #End Region End Class
Â
Building the Assembly and Loading it into the FIM Portal
This is exactly the same for VB.NET.Â
Configuring the Activity in FIM
This section is a little different to the example because I didn’t use the namespaces. Also, as I followed the instructions on creating an activities library, my DLL name is different.
Activity Name | FIM.CustomWorkflowActivitiesLibrary.RequestLoggingActivity |
Assembly Name | FIM.CustomWorkflowActivitiesLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxx |
Type Name | FIM.CustomWorkflowActivitiesLibrary.RequestLoggingActivitySettingsPart |
Is Action Activity | Checked |
Is Authentication Activity | Checked |
Is Authorization Activity | Checked |
 Troubleshooting
See Things I’ve been learning about debugging custom workflows.
This rocks! Thanks for posting. I really wish they would post C# and VB examples so those of us that prefer VB don’t have to try and translate the code manually.
Hi,
Thanks for the posting.
I am getting compilation error “Compilation failed. Unable to load one or more of the requested types. Retrieve the loadexception properties for more details..”
Actually if i compile my project without the Class file [for UI], build gets succeeded and i can see my dll. If i Build with the class file, my workflow activity libary is cleanedup and it never created again.
My target processor is Any CPU. Am building this project in x64 processor [64 bit] using VS2008 [.net 3.5]
Am i missing anything?
Thanks for your help.
I haven’t seen that one myself. I put a few troubleshooting tips here: https://www.wapshere.com/missmiis/things-ive-been-learning-about-debugging-custom-workflows
That error is mentioned on Joe Zamora’s blog – any chance you referenced an incorrect dll in the UI part? http://c–shark.blogspot.com/2010/03/compilation-failed-unable-to-load-one.html
Thats Correct. I recently installed update1 and didnt generate the WFExtension and webControls dll. Now i extracted the new dlls and it worked. Great Help! Thanks