Wednesday, July 16, 2008

Creating A Debugger Visualizer For Dynamic Entities

Simon Hutson wrote this on Tuesday, July 08, 2008 9:12 PM


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

No comments: