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.
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.
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.
No comments:
Post a Comment