Creating A Debugger Visualizer For Dynamic Entities

Are You Gonna Be My Girl....

Over the last couple of months I've been spending quite a bit of time with the SDK, building custom workflow activities and plug-ins for CRM 4.0. One of the problems working with the IPlugin interface is that you are forced to use the DynamicEntity class when accessing the InputParameters and OutputParameters properties of the plug-in context.

This wouldn't be so bad, except that it is a real pain trying to find out what properties and values are actually contained within a Dynamic Entity whilst debugging and so I kept having to write the same "test harness" code over and over again just to examine their contents. However, last week I found out you can actually extend Visual Studio to provide this support by implementing a custom debugger visualizer.

Custom debugger visualizers were introduced in Visual Studio 2005 and enable you to view the contents of an object or variable in a meaningful way whilst debugging your application. Out of the box, Visual Studio ships with four standard visualizers, including the "Text Visualizer", "HTML Visualizer", and "XML Visualizer", all of which work on string objects, and the "DataSet Visualizer", which works for DataSet, DataView, and DataTable objects. However, none of these understand the how to work with the complexities of DynamicEntity object.

Fortunately there is plenty of information available online (how to write a visualizer, Creating Debugger Visualizers with Visual Studio 2005), and even a good Channel9 video interview with the Visual Studio program manager responsible for this feature - Scott Nonnenberg - Visualizers in VS 2005. This is well worth watching if you want to really understand how you can improve your debugging experience.

After playing around with this for a few hours I came up with a really cool solution using a TreeView and DataGridView control to provide detailed drill-down into dynamic entities. As you can see below, whenever the Visual Studio comes across an object of type Microsoft.Crm.Sdk.DynamicEntity, the visualizer is represented in the debugger by a magnifying glass icon. When you see the magnifying glass in a DataTip, in a debugger variables window or in the QuickWatch dialog box, you can click on the magnifying glass to select the DynamicEntity Visualizer.

Calling The DynamicEntity Visualizer

Once launched, the DynamicEntity Visualizer iterates through all the dynamic entity properties to build a tree view on the left side of the form, showing the property name and the property type. Clicking on each property, displays the property attributes in a table on the right side of the form.

Using The DynamicEntity Visualizer

At a basic level, a custom debugger visualizer is just a Windows Forms application that is launched by Visual Studio, with the object being debugged passed as a parameter.

The basic code you need to implement the visualizer is shown below. You will need to get a reference to "Microsoft.VisualStudio.DebuggerVisualizer.dll" which ships with Visual Studio, but you do need to make sure you are using the correct version of this assembly depending on whether you are using Visual Studio 2005 or Visual Studio 2008.

 Imports System.ComponentModel
 Imports System.Windows.Forms
 Imports Microsoft.VisualStudio.DebuggerVisualizers
 Imports Microsoft.Crm.Sdk
  
 <Assembly: DebuggerVisualizer(GetType(DynamicEntityVisualizer), GetType(VisualizerObjectSource), Target:=GetType(Microsoft.Crm.Sdk.DynamicEntity), Description:="DynamicEntity Visualizer")> 
  
 Public Class DynamicEntityVisualizer
     Inherits DialogDebuggerVisualizer
  
     Protected Overrides Sub Show(ByVal windowService As IDialogVisualizerService, ByVal objectProvider As IVisualizerObjectProvider)
  
         Dim de As DynamicEntity = CType(objectProvider.GetObject, DynamicEntity)
  
         ' Process dynamic entity and display windows form here
  
     End Sub
  
 End Class

The <Assembly> attribute is used by Visual Studio to associate the Microsoft.Crm.Sdk.DynamicEntity type with the Visualizer, and once compiled, you can simply copy your assembly to the folder "C:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers\". Visual Studio will automatically use any visualizers it finds in this folder.

To build the user interface, simply create a new Windows Form in the project, add a "SplitContainer" control and place a "TreeView" control on the left side of the split and a "DataGridView" control on the right.

Next, create a "DynamicEntityProperty" class that we can use to store property attribute names and values.

 Friend Class DynamicEntityProperty
  
     Private _propertyName As String
     Public Property PropertyName() As String
         Get
             Return _propertyName
         End Get
         Set(ByVal value As String)
             _propertyName = value
         End Set
     End Property
  
     Private _propertyValue As String
     Public Property PropertyValue() As String
         Get
             Return _propertyValue
         End Get
         Set(ByVal value As String)
             _propertyValue = value
         End Set
     End Property
  
     Public Sub New(ByVal name As String, ByVal value As String)
         _propertyName = name
         _propertyValue = value
     End Sub
  
 End Class

Next we have to implement the logic that iterates through the dynamic entity and populates the "TreeView" control as shown below.

 Private Function AddDynamicEntityToTreeNode(ByVal node As TreeNode, ByVal entity As DynamicEntity) As TreeNode
  
     For Each [property] In entity.Properties
         Dim childNode As New TreeNode([property].Name + " (" + [property].GetType.Name + ")")
         If TypeOf [property] Is DynamicEntityArrayProperty Then
             For Each childEntity In CType([property], DynamicEntityArrayProperty).Value
                 childNode = AddDynamicEntityToTreeNode(childNode, childEntity)
             Next
         Else
             childNode = AddDynamicEntityPropertyToTreeNode(childNode, [property])
         End If
         node.Nodes.Add(childNode)
     Next
  
     Return node
  
 End Function

Once we have a "TreeView" with one or more "TreeNode" objects, we have to implement the logic that iterates through the attributes for each individual property, and add them to the TreeNode.Tag property as shown below. To keep the code short, and avoid writing the same For...Next code for each CRM property type, I decided to make use of .NET Reflection within the "AddDynamicEntityPropertyValueToTreeNode" function.

 Private Function AddDynamicEntityPropertyToTreeNode(ByVal node As TreeNode, ByVal [property] As [Property]) As TreeNode
  
     If TypeOf [property] Is CrmBooleanProperty Then
         Dim p = CType([property], CrmBooleanProperty)
         Dim value = CType(p.Value, CrmBoolean)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CrmDateTimeProperty Then
         Dim p = CType([property], CrmDateTimeProperty)
         Dim value = CType(p.Value, CrmDateTime)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CrmDecimalProperty Then
         Dim p = CType([property], CrmDecimalProperty)
         Dim value = CType(p.Value, CrmDecimal)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CrmFloatProperty Then
         Dim p = CType([property], CrmFloatProperty)
         Dim value = CType(p.Value, CrmFloat)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CrmMoneyProperty Then
         Dim p = CType([property], CrmMoneyProperty)
         Dim value = CType(p.Value, CrmMoney)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CrmNumberProperty Then
         Dim p = CType([property], CrmNumberProperty)
         Dim value = CType(p.Value, CrmNumber)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is CustomerProperty Then
         Dim p = CType([property], CustomerProperty)
         Dim value = CType(p.Value, Customer)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is EntityNameReferenceProperty Then
         Dim p = CType([property], EntityNameReferenceProperty)
         Dim value = CType(p.Value, EntityNameReference)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is KeyProperty Then
         Dim p = CType([property], KeyProperty)
         Dim value = CType(p.Value, Key)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is LookupProperty Then
         Dim p = CType([property], LookupProperty)
         Dim value = CType(p.Value, Lookup)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is OwnerProperty Then
         Dim p = CType([property], OwnerProperty)
         Dim value = CType(p.Value, Owner)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is PicklistProperty Then
         Dim p = CType([property], PicklistProperty)
         Dim value = CType(p.Value, Picklist)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is StateProperty Then
         Dim p = CType([property], StateProperty)
         Dim value = CType(p.Value, String)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is StatusProperty Then
         Dim p = CType([property], StatusProperty)
         Dim value = CType(p.Value, Status)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is StringProperty Then
         Dim p = CType([property], StringProperty)
         Dim value = CType(p.Value, String)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     ElseIf TypeOf [property] Is UniqueIdentifierProperty Then
         Dim p = CType([property], UniqueIdentifierProperty)
         Dim value = CType(p.Value, UniqueIdentifier)
         node = AddDynamicEntityPropertyValueToTreeNode(node, value)
     Else
         Throw New ArgumentException("Property type is not supported.", "property")
     End If
  
     Return node
  
 End Function
  
 Private Function AddDynamicEntityPropertyValueToTreeNode(ByVal node As TreeNode, ByVal value As Object) As TreeNode
  
     ' Create a list of DynamicEntityProperty
     Dim values As New BindingList(Of DynamicEntityProperty)
  
     ' Check this is not a string or a value type (i.e. this is a reference type)
     If Not (TypeOf value Is String) And Not (Type.GetType(value.GetType.AssemblyQualifiedName).IsValueType) Then
         ' Use reflection to get the public instance properties and their values from each property of DynamicEntity.value
         For Each propertyInfo In Type.GetType(value.GetType.AssemblyQualifiedName).GetProperties(Reflection.BindingFlags.Public Or Reflection.BindingFlags.Instance)
             ' Check that the property can be read.
             If propertyInfo.CanRead Then
                 Try
                     values.Add(New DynamicEntityProperty(propertyInfo.Name, GetString(propertyInfo.GetValue(value, Nothing))))
                 Catch ex As Exception
                     ' An unknown error occured, so ignore it and do nothing
                 End Try
             End If
         Next
     Else
         values.Add(New DynamicEntityProperty("Value", value.ToString))
     End If
  
     ' Add the list to the TreeNode.Tag property
     node.Tag = values
  
     Return node
  
 End Function
  
 Private Function GetString(ByVal value As Object) As String
  
     If value Is Nothing Then
         Return String.Empty
     Else
         If TypeOf value Is Guid Then
             Return CType(value, Guid).ToString
         ElseIf TypeOf value Is Boolean Then
             Return value.ToString
         ElseIf TypeOf value Is Integer Then
             Return value.ToString
         ElseIf TypeOf value Is Date Then
             Return value.ToString
         ElseIf TypeOf value Is String Then
             Return value.ToString
         Else
             Return String.Empty
         End If
     End If
  
 End Function

Finally, the last piece of code is needed to update the "DataGridView" by binding data stored in the "TreeNode.Tag" property when selecting a Dynamic Entity property in the "TreeView". We can also take this opportunity to set some properties on the "DataGridView" in order to make the table format look nice.

 Imports System.Windows.Forms
 Imports System.ComponentModel
  
 Public Class DynamicEntityVisualizerForm
  
     Private Sub DynamicEntityTreeView_AfterSelect(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles DynamicEntityTreeView.AfterSelect
  
         PropertyDataGridView.DataSource = Nothing
  
         If Not (e.Node.Tag Is Nothing) Then
             PropertyDataGridView.DataSource = CType(e.Node.Tag, BindingList(Of DynamicEntityProperty))
             PropertyDataGridView.Columns.Item(0).AutoSizeMode = DataGridViewAutoSizeColumnMode.None
             PropertyDataGridView.Columns.Item(0).MinimumWidth = 100
             PropertyDataGridView.Columns.Item(0).Name = "Property Name"
             PropertyDataGridView.Columns.Item(1).AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill
             PropertyDataGridView.Columns.Item(1).MinimumWidth = 100
             PropertyDataGridView.Columns.Item(1).Name = "Property Value"
             PropertyDataGridView.ClearSelection()
         End If
  
     End Sub
  
     Private Sub DynamicEntityVisualizerForm_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
  
         PropertyDataGridView.Enabled = True
         PropertyDataGridView.SelectionMode = DataGridViewSelectionMode.CellSelect
         PropertyDataGridView.ClipboardCopyMode = DataGridViewClipboardCopyMode.EnableWithoutHeaderText
         PropertyDataGridView.RowHeadersVisible = False
         PropertyDataGridView.RowsDefaultCellStyle.WrapMode = DataGridViewTriState.True
         PropertyDataGridView.RowsDefaultCellStyle.Alignment = DataGridViewContentAlignment.TopLeft
         PropertyDataGridView.AutoSizeRowsMode = DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders
  
     End Sub
  
 End Class

So there we have it. A really useful piece of functionality that saves time and effort when debugging your plug-ins and custom workflow assemblies. I built this solution in Visual Studio 2008, for debugging in Visual Studio 2008 and you can download the project files here.

If you want to use this "AS IS" you can just copy file "SRH.CRM.VisualStudio.DebuggerVisualizer.dll" from the \bin\debug project folder to the folder "C:\Program Files\Microsoft Visual Studio 9.0\Common7\Packages\Debugger\Visualizers\". However, if you do wish to modify the functionality, or re-compile it to run under Visual Studio 2005, then please feel free to do so.

This posting is provided "AS IS" with no warranties, and confers no rights.

Laughing Boy

SRH.CRM.VisualStudio.DebuggerVisualizer.zip