Search code examples
c#vb.netpropertygridtypedescriptor

Expand collection in property grid without any modifications to the class?


This approach works for everything but collections:

Collections are displayed like this:

enter image description here

So even though they are expandable, there isn't much use for them inside property grid.

Here is an example of what I am looking for (screenshot taken from here):

enter image description here

The linked article also contains some code, which would make this happen, but it requires modifying the original class. Between it and my previous question, I came up with some ideas, but I'm not very fluent in using System.ComponentModel namespace.

Here is a reduced test case (custom class with one property of collection type, which contains one object of custom type, which has one string property):

Imports System.ComponentModel

Public Class Form1

  Sub New()
    ' This call is required by the designer.
    InitializeComponent()

    ' Add any initialization after the InitializeComponent() call.
    Me.AssignTypeConverter(Of MyCustomClassCollection, ExpandableObjectConverter)()
  End Sub

  Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
    Dim collection As New MyCustomClassCollection
    collection.Add(New MyCustomClass With {.MyCustomProperty = "Hello"})

    Dim container As New MyCustomClassCollectionContainer(collection)

    Me.PropertyGrid1.SelectedObject = container
  End Sub

  Private Sub AssignTypeConverter(Of IType, IConverterType)()
    System.ComponentModel.TypeDescriptor.AddAttributes(GetType(IType),
      New System.ComponentModel.TypeConverterAttribute(GetType(IConverterType)))
  End Sub

End Class


Public Class MyCustomClass
  Public Property MyCustomProperty As String
End Class

Public Class MyCustomClassCollection : Inherits System.Collections.ObjectModel.Collection(Of MyCustomClass)
End Class

Public Class MyCustomClassCollectionContainer

  Dim _items As MyCustomClassCollection

  Public ReadOnly Property Items As MyCustomClassCollection
    Get
      Return _items
    End Get
  End Property

  Sub New(items As MyCustomClassCollection)
    _items = items
  End Sub

End Class

Proposed solution (pseudo-code, does not compile)

Imports System.ComponentModel

Public Class MyCustomClassTypeDescriptor : Inherits ExpandableObjectConverter

  Public Overrides Function GetProperties(context As ITypeDescriptorContext,
                                value As Object, attributes() As Attribute) _
                                            As PropertyDescriptorCollection
    Dim pds As New PropertyDescriptorCollection(Nothing)
    Dim lst As IList(Of Object) = DirectCast(value, IList)
    For i As Integer = 0 To lst.Count - 1
      Dim item As MyCustomClass = DirectCast(lst.Item(i), MyCustomClass)
      'compile error - abstract class cannot be instantiated
      Dim pd As New PropertyDescriptor(item)
      pds.Add(pd)
    Next
    Return pds
  End Function

End Class

And then apply this custom object converter at runtime.

Is it going to work like this? What am I missing? Any suggestions are welcome!

Note: The above is VB.NET, but if you speak C#, feel free to use it.


Solution

  • Too long to continue in the comments, but how about something like this - a custom ExpandableObjectConverter that turns each collection item into a property (ItemX), and a custom property descriptor that gets the appropriate item.

    Public Class MyCollectionTypeDescriptor(Of TColl As Collection(Of TItem), TItem)
        Inherits ExpandableObjectConverter
    
        Public Overrides Function GetProperties(context As ITypeDescriptorContext, value As Object, attributes() As Attribute) As PropertyDescriptorCollection
            Dim coll = DirectCast(value, TColl)
            Dim props(coll.Count - 1) As PropertyDescriptor
            For i = 0 To coll.Count - 1
                props(i) = New MyCollectionPropertyDescriptor(Of TColl, TItem)("Item" & CStr(i))
            Next
            Return New PropertyDescriptorCollection(props)
        End Function
    
    End Class
    
    Public Class MyCollectionPropertyDescriptor(Of TColl, TItem)
        Inherits PropertyDescriptor
    
        Private _index As Integer = 0
    
        Public Sub New(name As String)
            MyBase.New(name, Nothing)
            Dim indexStr = Regex.Match(name, "\d+$").Value
            _index = CInt(indexStr)
        End Sub
    
        Public Overrides Function CanResetValue(component As Object) As Boolean
            Return False
        End Function
    
        Public Overrides ReadOnly Property ComponentType As Type
            Get
                Return GetType(TColl)
            End Get
        End Property
    
        Public Overrides Function GetValue(component As Object) As Object
            Dim coll = DirectCast(component, Collection(Of TItem))
            Return coll(_index)
        End Function
    
        Public Overrides ReadOnly Property IsReadOnly As Boolean
            Get
                Return True
            End Get
        End Property
    
        Public Overrides ReadOnly Property PropertyType As Type
            Get
                Return GetType(TItem)
            End Get
        End Property
    
        Public Overrides Sub ResetValue(component As Object)
    
        End Sub
    
        Public Overrides Sub SetValue(component As Object, value As Object)
    
        End Sub
    
        Public Overrides Function ShouldSerializeValue(component As Object) As Boolean
            Return False
        End Function
    
    End Class
    

    You can associate everything to your classes using:

    Me.AssignTypeConverter(Of MyCustomClass, ExpandableObjectConverter)()
    Me.AssignTypeConverter(Of MyCustomClassCollection, MyCollectionTypeDescriptor(Of MyCustomClassCollection, MyCustomClass))()
    

    That should list each item in the main property grid, and each item will be expandable inline. Is that what you are looking for?