The FIM 2010 Custom Logging Activity in VB.NET

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.

4 Replies to “The FIM 2010 Custom Logging Activity in VB.NET”

  1. 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.

  2. 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.

  3. 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

Comments are closed.