{"id":894,"date":"2010-08-14T13:14:08","date_gmt":"2010-08-14T13:14:08","guid":{"rendered":"https:\/\/www.wapshere.com\/missmiis\/?p=894"},"modified":"2011-01-15T15:25:40","modified_gmt":"2011-01-15T15:25:40","slug":"the-fim-2010-custom-logging-activity-in-vb-net","status":"publish","type":"post","link":"https:\/\/www.wapshere.com\/missmiis\/the-fim-2010-custom-logging-activity-in-vb-net","title":{"rendered":"The FIM 2010 Custom Logging Activity in VB.NET"},"content":{"rendered":"<p>Here&#8217;s how I got the <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ff859524.aspx\">Custom Workflow Logging example<\/a> working in VB.NET. Please consult this post together with the Microsoft document as I&#8217;m not going to reproduce the entire thing here. The usual warnings about me being no great developer also apply.<!--more-->\u00c2\u00a0<\/p>\n<h3>Project Naming<\/h3>\n<p>If you want to use this as a starting point to writing more workflow activities then you should start by following the document <a href=\"http:\/\/msdn.microsoft.com\/en-us\/library\/ee652293.aspx\">How to: Create a Custom Activity Library<\/a>, making the following changes for VB.NET:\u00c2\u00a0<\/p>\n<p>1. Obviously, wherever it says &#8220;Visual C#&#8221; or &#8220;.vc&#8221; just using the VB.NET alternatives.\u00c2\u00a0<\/p>\n<p>2. The &#8220;Application&#8221; tab is different for VB.NET:\u00c2\u00a0<\/p>\n<ul>\n<li>For the\u00c2\u00a0<strong>Assembly Name<\/strong>\u00c2\u00a0I ended\u00c2\u00a0up with\u00c2\u00a0&#8220;FIM.CustomWorkflowActivitiesLibrary&#8221;<\/li>\n<li>There is no &#8220;Default namespace&#8221; setting that I can find. Instead\u00c2\u00a0I set\u00c2\u00a0<strong>Root namespace<\/strong> to &#8220;FIM.CustomWorkflowActivitiesLibrary&#8221;.<\/li>\n<li>You will find the <strong>Target Framework<\/strong> setting under Compile -&gt; Advanced Compile Options.<\/li>\n<\/ul>\n<p>For reference, my solution ends up looking like this:\u00c2\u00a0<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.wapshere.com\/images\/fim_wf_logging.jpg\" alt=\"\" \/>\u00c2\u00a0<\/p>\n<p>and the Object Browser shows these namespaces and classes:\u00c2\u00a0<\/p>\n<p><img decoding=\"async\" src=\"https:\/\/www.wapshere.com\/images\/fim_wf_loggingnamespaces.jpg\" alt=\"\" \/>\u00c2\u00a0<\/p>\n<h3>Build Settings<\/h3>\n<p>The walkthrough tells you how to add post-build steps so you don&#8217;t need to go through the process of putting your dll in the GAC and restarting FIM\u00c2\u00a0each 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 -&gt; Build Events. Paste the following lines in to the &#8220;Post-build event command line&#8221; box:<br \/>\n<code><br \/>\n\"C:\\Program Files\\Microsoft SDKs\\Windows\\v6.0A\\Bin\\gacutil.exe\" \/i \"$(TargetPath)\"<br \/>\nnet stop \"Forefront Identity Manager Service\"<br \/>\nnet start \"Forefront Identity Manager Service\"<br \/>\n<\/code>\u00c2\u00a0<\/p>\n<h3>Adding FIM Out-of-Box Activities to the Toolbox<\/h3>\n<p>This section is basically the same for both VC# and VB.NET.\u00c2\u00a0<\/p>\n<p>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&#8217;t bother clearing the existing actions &#8211; just add your FIM ones.\u00c2\u00a0<\/p>\n<h3>Defining the activity sequence and properties<\/h3>\n<p>Here&#8217;s the RequestLoggingActivity code in VB.NET.\u00c2\u00a0<\/p>\n<pre>Imports System.Collections.Generic\r\nImports System.Collections.ObjectModel\r\nImports System.IO\r\nImports Microsoft.ResourceManagement.WebServices.WSResourceManagement\r\nImports Microsoft.ResourceManagement.Workflow.Activities\r\n\r\nPublic Class RequestLoggingActivity\r\n    Inherits SequenceActivity\r\n\r\n#Region \"Public Workflow Properties\"\r\n\r\n    Public Shared ReadCurrentRequestActivity_CurrentRequestProperty As DependencyProperty = DependencyProperty.Register(\"ReadCurrentRequestActivity_CurrentRequest\", GetType(Microsoft.ResourceManagement.WebServices.WSResourceManagement.RequestType), GetType(RequestLoggingActivity))\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Stores information about the current request\r\n    ''' &lt;\/summary&gt;\r\n    &lt;DesignerSerializationVisibilityAttribute(DesignerSerializationVisibility.Visible)&gt; _\r\n    &lt;BrowsableAttribute(True)&gt; _\r\n    &lt;CategoryAttribute(\"Misc\")&gt; _\r\n    Public Property ReadCurrentRequestActivity_CurrentRequest() As RequestType\r\n        Get\r\n            Return DirectCast(MyBase.GetValue(ReadCurrentRequestActivity_CurrentRequestProperty), Microsoft.ResourceManagement.WebServices.WSResourceManagement.RequestType)\r\n        End Get\r\n        Set(ByVal value As RequestType)\r\n            MyBase.SetValue(RequestLoggingActivity.ReadCurrentRequestActivity_CurrentRequestProperty, value)\r\n        End Set\r\n    End Property\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Identifies the Log File Path\r\n    ''' &lt;\/summary&gt;\r\n    Public Shared LogFilePathProperty As DependencyProperty = DependencyProperty.Register(\"LogFilePath\", GetType(System.String), GetType(RequestLoggingActivity))\r\n    &lt;Description(\"Please specify the Log File Path\")&gt; _\r\n    &lt;DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)&gt; _\r\n    &lt;Browsable(True)&gt; _\r\n    Public Property LogFilePath() As String\r\n        Get\r\n            Return DirectCast(MyBase.GetValue(RequestLoggingActivity.LogFilePathProperty), [String])\r\n        End Get\r\n        Set(ByVal value As String)\r\n            MyBase.SetValue(RequestLoggingActivity.LogFilePathProperty, value)\r\n        End Set\r\n    End Property\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Identifies the Log File Name\r\n    ''' &lt;\/summary&gt;\r\n    Public Shared LogFileNameProperty As DependencyProperty = DependencyProperty.Register(\"LogFileName\", GetType(System.String), GetType(RequestLoggingActivity))\r\n    &lt;Description(\"Please specify the Log File Path\")&gt; _\r\n    &lt;DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)&gt; _\r\n    &lt;Browsable(True)&gt; _\r\n    Public Property LogFileName() As String\r\n        Get\r\n            Return DirectCast(MyBase.GetValue(RequestLoggingActivity.LogFileNameProperty), [String])\r\n        End Get\r\n        Set(ByVal value As String)\r\n            MyBase.SetValue(RequestLoggingActivity.LogFileNameProperty, value)\r\n        End Set\r\n    End Property\r\n#End Region\r\n\r\n#Region \"Execution Logic\"\r\n    ''' &lt;summary&gt;\r\n    ''' Defines to logic of the LogRequestDataToFile activity.\r\n    ''' This code will be executed when the LogRequestDataToFile activity\r\n    ''' becomes to active workflow.\r\n    ''' &lt;\/summary&gt;\r\n    ''' &lt;param name=\"sender\"&gt;&lt;\/param&gt;\r\n    ''' &lt;param name=\"e\"&gt;&lt;\/param&gt;\r\n    ''' &lt;remarks&gt;&lt;\/remarks&gt;\r\n    Private Sub LogRequestDataToFile_ExecuteCode(ByVal sender As System.Object, ByVal e As System.EventArgs)\r\n        'Get current request from previous activity\r\n        Dim currentRequest As RequestType = Me.ReadCurrentRequestActivity_CurrentRequest\r\n\r\n        Try\r\n            ' Output the Request type and object type\r\n            Me.Log(\"Request Operation: \" &amp; currentRequest.Operation)\r\n            Me.Log(\"Target Object Type: \" &amp; currentRequest.TargetObjectType)\r\n\r\n            ' As UpdateRequestParameter derives from CreateRequestParameter we can simplify the code by deriving\r\n            ' from CreateRequestParameter only.\r\n            Dim requestParameters As ReadOnlyCollection(Of CreateRequestParameter) = currentRequest.ParseParameters(Of CreateRequestParameter)()\r\n\r\n            ' Loop through CreateRequestParameters and print out each attribute\/value pair\r\n            Me.Log(\"Parameters for request: \" + currentRequest.ObjectID.ToString)\r\n            For Each requestParameter As CreateRequestParameter In requestParameters\r\n                If requestParameter.Value IsNot Nothing Then\r\n                    Me.Log((\"     \" + requestParameter.PropertyName &amp; \": \") &amp; requestParameter.Value.ToString())\r\n                End If\r\n            Next\r\n\r\n            Dim containingWorkflow As SequentialWorkflow = Nothing\r\n            ' In order to read the Workflow Dictionary we need to get the containing (parent) workflow\r\n            If Not SequentialWorkflow.TryGetContainingWorkflow(Me, containingWorkflow) Then\r\n                Throw New InvalidOperationException(\"Unable to get Containing Workflow\")\r\n            End If\r\n            Me.Log(\"Containing Workflow Dictionary (WorkflowData):\")\r\n            ' Loop through Workflow Dictionary and log each attribute\/value pair\r\n            For Each item As KeyValuePair(Of String, Object) In containingWorkflow.WorkflowDictionary\r\n                Me.Log((\"     \" &amp; item.Key &amp; \": \") &amp; item.Value.ToString())\r\n            Next\r\n            Me.Log(vbLf &amp; vbLf)\r\n        Catch ex As Exception\r\n            Me.Log(\"Logging Activity Exception Thrown: \" &amp; ex.Message)\r\n        End Try\r\n    End Sub\r\n#End Region\r\n\r\n#Region \"Utility Functions\"\r\n\r\n    ' Prefix the current time to the message and log the message to the log file.\r\n    Private Sub Log(ByVal message As String)\r\n        Using log As New StreamWriter(Path.Combine(Me.LogFilePath, Me.LogFileName), True)\r\n            'since the previous line is part of a \"using\" block, the file will automatically\r\n            'be closed (even if writing to the file caused an exception to be thrown).\r\n            'For more information see\r\n            ' http:\/\/msdn.microsoft.com\/en-us\/library\/yh598w02.aspx\r\n            log.WriteLine(DateTime.Now.ToString(\"yyyy-MM-dd hh:mm:ss\") + \": \" &amp; message)\r\n        End Using\r\n    End Sub\r\n\r\n#End Region\r\nEnd Class<\/pre>\n<p>\u00c2\u00a0\u00c2\u00a0<\/p>\n<h3>Creating a User Interface for the Activity<\/h3>\n<p>Here&#8217;s the code for this section in VB.NET. Note that I have changed the Namespace definition to get round the VB.NET &#8220;Root namespace&#8221; setting working differently to the VC# &#8220;Default namespace&#8221; setting.\u00c2\u00a0<br \/>\n\u00c2\u00a0\u00c2\u00a0<\/p>\n<pre>Imports System\r\nImports System.Collections.Generic\r\nImports System.Linq\r\nImports System.Text\r\nImports System.Web.UI.WebControls\r\nImports System.Workflow.ComponentModel\r\nImports Microsoft.IdentityManagement.WebUI.Controls\r\nImports Microsoft.ResourceManagement.Workflow.Activities\r\nImports FIM.CustomWorkflowActivitiesLibrary.Activities\r\n\r\nPublic Class RequestLoggingActivitySettingsPart\r\n    Inherits ActivitySettingsPart\r\n\r\n    ''' &lt;summary&gt;\r\n    ''' Called when a user clicks the Save button in the Workflow Designer.\r\n    ''' Returns an instance of the RequestLoggingActivity class that\r\n    ''' has its properties set to the values entered into the text box controls\r\n    ''' used in the UI of the activity.\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Function GenerateActivityOnWorkflow(ByVal workflow As SequentialWorkflow) As Activity\r\n        If Not Me.ValidateInputs() Then\r\n            Return Nothing\r\n        End If\r\n        Dim LoggingActivity As New RequestLoggingActivity()\r\n        LoggingActivity.LogFilePath = Me.GetText(\"txtLogFilePath\")\r\n        LoggingActivity.LogFileName = Me.GetText(\"txtLogFileName\")\r\n        Return LoggingActivity\r\n    End Function\r\n\r\n    ''' &lt;summary&gt;\r\n    ''' Called when editing the workflow activity settings.\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Sub LoadActivitySettings(ByVal activity As Activity)\r\n        Dim LoggingActivity As RequestLoggingActivity = TryCast(activity, RequestLoggingActivity)\r\n        If LoggingActivity IsNot Nothing Then\r\n            Me.SetText(\"txtLogFilePath\", LoggingActivity.LogFilePath)\r\n            Me.SetText(\"txtLogFileName\", LoggingActivity.LogFileName)\r\n        End If\r\n    End Sub\r\n\r\n    ''' &lt;summary&gt;\r\n    ''' Saves the activity settings.\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Function PersistSettings() As ActivitySettingsPartData\r\n        Dim data As New ActivitySettingsPartData()\r\n        data(\"LogFilePath\") = Me.GetText(\"txtLogFilePath\")\r\n        data(\"LogFileName\") = Me.GetText(\"txtLogFileName\")\r\n        Return data\r\n    End Function\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Restores the activity settings in the UI\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Sub RestoreSettings(ByVal data As ActivitySettingsPartData)\r\n        If data IsNot Nothing Then\r\n            Me.SetText(\"txtLogFilePath\", DirectCast(data(\"LogFilePath\"), String))\r\n            Me.SetText(\"txtLogFileName\", DirectCast(data(\"LogFileName\"), String))\r\n        End If\r\n    End Sub\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Switches the activity between read only and read\/write mode\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Sub SwitchMode(ByVal mode As ActivitySettingsPartMode)\r\n        Dim [readOnly] As Boolean = (mode = ActivitySettingsPartMode.View)\r\n        Me.SetTextBoxReadOnlyOption(\"txtLogFilePath\", [readOnly])\r\n        Me.SetTextBoxReadOnlyOption(\"txtLogFileName\", [readOnly])\r\n    End Sub\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Returns the activity name.\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides ReadOnly Property Title() As String\r\n        Get\r\n           Return \"Request Logging Activity\"\r\n        End Get\r\n    End Property\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  In general, this method should be used to validate information entered\r\n    '''  by the user when the activity is added to a workflow in the Workflow\r\n    '''  Designer.\r\n    '''  We could add code to verify that the log file path already exists on\r\n    '''  the server that is hosting the FIM Portal and check that the activity\r\n    '''  has permission to write to that location. However, the code\r\n    '''  would only check if the log file path exists when the\r\n    '''  activity is added to a workflow in the workflow designer. This class\r\n    '''  will not be used when the activity is actually run.\r\n    '''  For this activity we will just return true.\r\n    ''' &lt;\/summary&gt;\r\n    Public Overrides Function ValidateInputs() As Boolean\r\n        Return True\r\n    End Function\r\n\r\n    ''' &lt;summary&gt;\r\n    '''  Creates a Table that contains the controls used by the activity UI\r\n    '''  in the Workflow Designer of the FIM portal. Adds that Table to the\r\n    '''  collection of Controls that defines each activity that can be selected\r\n    '''  in the Workflow Designer of the FIM Portal. Calls the base class of\r\n    '''  ActivitySettingsPart to render the controls in the UI.\r\n    ''' &lt;\/summary&gt;\r\n    Protected Overrides Sub CreateChildControls()\r\n        Dim controlLayoutTable As Table\r\n        controlLayoutTable = New Table()\r\n\r\n        'Width is set to 100% of the control size\r\n        controlLayoutTable.Width = Unit.Percentage(100.0)\r\n        controlLayoutTable.BorderWidth = 0\r\n        controlLayoutTable.CellPadding = 2\r\n        'Add a TableRow for each textbox in the UI\r\n        controlLayoutTable.Rows.Add(Me.AddTableRowTextBox(\"Log File Path:\", \"txtLogFilePath\", 400, 100, False, \"Enter the log file Path.\"))\r\n        controlLayoutTable.Rows.Add(Me.AddTableRowTextBox(\"Log File Name:\", \"txtLogFileName\", 400, 100, False, \"Enter the log file Name.\"))\r\n        Me.Controls.Add(controlLayoutTable)\r\n\r\n        MyBase.CreateChildControls()\r\n    End Sub\r\n\r\n#Region \"Utility Functions\"\r\n    'Create a TableRow that contains a label and a textbox.\r\n    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\r\n        Dim row As New TableRow()\r\n        Dim labelCell As New TableCell()\r\n        Dim controlCell As New TableCell()\r\n        Dim oLabel As New Label()\r\n        Dim oText As New TextBox()\r\n\r\n        oLabel.Text = labelText\r\n        oLabel.CssClass = MyBase.LabelCssClass\r\n        labelCell.Controls.Add(oLabel)\r\n        oText.ID = controlID\r\n        oText.CssClass = MyBase.TextBoxCssClass\r\n        oText.Text = defaultValue\r\n        oText.MaxLength = maxLength\r\n        oText.Width = width\r\n        If multiLine Then\r\n            oText.TextMode = TextBoxMode.MultiLine\r\n            oText.Rows = System.Math.Min(6, (maxLength + 60) \\ 60)\r\n            oText.Wrap = True\r\n        End If\r\n        controlCell.Controls.Add(oText)\r\n        row.Cells.Add(labelCell)\r\n        row.Cells.Add(controlCell)\r\n        Return row\r\n    End Function\r\n\r\n    Private Function GetText(ByVal textBoxID As String) As String\r\n        Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox)\r\n        Return If(textBox.Text, [String].Empty)\r\n    End Function\r\n\r\n    Private Sub SetText(ByVal textBoxID As String, ByVal text As String)\r\n        Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox)\r\n        If textBox IsNot Nothing Then\r\n            textBox.Text = text\r\n        Else\r\n            textBox.Text = \"\"\r\n        End If\r\n    End Sub\r\n\r\n    'Set the text box to read mode or read\/write mode\r\n    Private Sub SetTextBoxReadOnlyOption(ByVal textBoxID As String, ByVal [readOnly] As Boolean)\r\n        Dim textBox As TextBox = DirectCast(Me.FindControl(textBoxID), TextBox)\r\n        textBox.[ReadOnly] = [readOnly]\r\n    End Sub\r\n#End Region\r\n\r\nEnd Class<\/pre>\n<p>\u00c2\u00a0<\/p>\n<h3>Building the Assembly and Loading it into the FIM Portal<\/h3>\n<p>This is exactly the same for VB.NET.\u00c2\u00a0<\/p>\n<h3>Configuring the Activity in FIM<\/h3>\n<p>This section is a little different to the\u00c2\u00a0example\u00c2\u00a0because I didn&#8217;t use the namespaces. Also, as I followed the instructions on creating an activities library, my DLL name is different.<\/p>\n<table>\n<tbody>\n<tr>\n<td>Activity Name<\/td>\n<td>FIM.CustomWorkflowActivitiesLibrary.RequestLoggingActivity<\/td>\n<\/tr>\n<tr>\n<td>Assembly Name<\/td>\n<td>FIM.CustomWorkflowActivitiesLibrary, Version=1.0.0.0, Culture=neutral, PublicKeyToken=<em>xxxx<\/em><\/td>\n<\/tr>\n<tr>\n<td>Type Name<\/td>\n<td>FIM.CustomWorkflowActivitiesLibrary.RequestLoggingActivitySettingsPart<\/td>\n<\/tr>\n<tr>\n<td>Is Action Activity<\/td>\n<td>Checked<\/td>\n<\/tr>\n<tr>\n<td>Is Authentication Activity<\/td>\n<td>Checked<\/td>\n<\/tr>\n<tr>\n<td>Is Authorization Activity<\/td>\n<td>Checked<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h3>\u00c2\u00a0Troubleshooting<\/h3>\n<p>See <a href=\"https:\/\/www.wapshere.com\/missmiis\/things-ive-been-learning-about-debugging-custom-workflows\">Things I\u00e2\u20ac\u2122ve been learning about debugging custom workflows<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Here&#8217;s how I got the Custom Workflow Logging example working in VB.NET. Please consult this post together with the Microsoft document as I&#8217;m not going to reproduce the entire thing here. The usual warnings about me being no great developer also apply.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"footnotes":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":[]},"categories":[42,30,45],"tags":[],"class_list":["post-894","post","type-post","status-publish","format-standard","hentry","category-fim-2010","category-vbnet","category-workflow"],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/pkp1o-eq","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/894","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/comments?post=894"}],"version-history":[{"count":13,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/894\/revisions"}],"predecessor-version":[{"id":1256,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/posts\/894\/revisions\/1256"}],"wp:attachment":[{"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/media?parent=894"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/categories?post=894"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.wapshere.com\/missmiis\/wp-json\/wp\/v2\/tags?post=894"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}