Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Handy Type Editors. Universal Dropdown Editor

0.00/5 (No votes)
5 May 2004 1  
Implementing a universal dropdown type editor.

Demo snapshot

Introduction

In my previous article, Handy Type Editors. Filename Editor, I demonstrated the use of custom attributes for altering the way an editor works. Today, we'll go further and implement a type editor controlled by both attributes and members of an edited type. The purpose of the exercise is creating a dropdown editor suitable for most occasions. Instead of writing nearly the same code over and over again, breaking the main commandment of pragmatic programming (Thou shalt not duplicate thy code), it is sufficient to configure the class by adding one or two attributes.

Main Idea

As mentioned in the previous article, a dropdown editor is meant to display a control below the cell of the edited member. Our control will be a ListBox displaying contents of a collection in the class being edited. The target collection is specified by an attribute. We'll also provide a way to configure what member of an item is displayed, and how the value is determined.

Let's Code!

Prerequisites

We need to provide a way to load our collection to a ListBox. ListControl has a built-in DataSource property which allows binding of an IList implementing object. However, there is a lot of collections out there that do not implement IList. We don't want to miss these either. This is why we'll write a small utility function which loads a collection to a ListBox. It will also act as a type validator (that is, an exception will be thrown if the user passes an invalid property).

    Public Shared Sub FillListBoxFromCollection(ByVal lb As ListBox, _
                ByVal coll As ICollection)
        ' prevent flickers and slow downs by entering the mass update mode

        lb.BeginUpdate()
        lb.Items.Clear()
        Dim item As Object
        For Each item in coll
            lb.Items.Add(item)
        Next
        lb.EndUpdate()
        lb.Invalidate()
    End Sub

Essential Attributes

As mentioned earlier, custom attributes are used to configure the editor:

  • SourceCollectionAttribute. It holds the name and the reference to the type member (or property, to be exact) which serves as a source of ListBox data. Implementation:
        <Description("Service attribute to point to the source collection."), _
                AttributeUsage(AttributeTargets.All)> _
        Public Class SourceCollectionAttribute
            Inherits Attribute
            Private srcCollName As String
    
            Public ReadOnly Property CollectionName() As String
                Get
                    Return Me.srcCollName
                End Get
            End Property
    
            Public ReadOnly Property Collection(ByVal instance As Object) _
                    As ICollection
                Get
                    Dim pdc As PropertyDescriptorCollection = _
                            TypeDescriptor.GetProperties(instance)
                    Dim pd As PropertyDescriptor
                    For Each pd In pdc
                        If pd.Name = Me.srcCollName Then
                            Return pd.GetValue(instance)
                        End If
                    Next
                    Return Nothing
                End Get
            End Property
    
            Public Sub New(ByVal sourceCollectionPropertyName As String)
                Me.srcCollName = sourceCollectionPropertyName
            End Sub
        End Class
  • ValueMemberAttribute. It holds the name of the listed items' type member which is assigned to the edited value. For the sake of simplicity, if the attribute is not used, the entire selected object is returned. Pay attention to GetValue and SelectByValue functions: we cannot use ListControl.SelectedValue since we're not binding the data through ListControl.DataSource. Implementation:
        <AttributeUsage(AttributeTargets.All)> _
        Public Class ValueMemberAttribute
            Inherits Attribute
            Private valMemb As String
    
            Public ReadOnly Property ValuePropertyName() As String
                Get
                    Return Me.valMemb
                End Get
            End Property
    
            Public Sub SelectByValue(ByVal lb As ListBox, ByVal val As Object)
                lb.SelectedItem = Nothing
                Dim item As Object
                For Each item In lb.Items
                    If Me.GetValue(item) = val Then
                        lb.SelectedItem = item
                        Exit Sub
                    End If
                Next
            End Sub
    
            Public Function GetValue(ByVal obj As Object) As Object
                If Me.valMemb = String.Empty Then Return obj
                Dim pi As System.Reflection.PropertyInfo = _
                  obj.GetType().GetProperty(Me.valMemb)
                If pi Is Nothing Then Return Nothing
                Return pi.GetValue(obj, Nothing)
            End Function
    
            Public Sub New(ByVal valueMemberPropertyName As String)
                Me.valMemb = valueMemberPropertyName
            End Sub
        End Class
  • DisplayMemberAttribute. It holds the name of the listed items' type member which is used to display items in the list. If omitted, the item's ToString method is used. All we must do is assign the string to the DisplayMember property of the ListBox we created. Implementation:
        <AttributeUsage(AttributeTargets.All)> _
        Public Class DisplayMemberAttribute
            Inherits Attribute
            Private dispMemb As String
    
            Public ReadOnly Property DisplayPropertyName() As String
                Get
                    Return Me.dispMemb
                End Get
            End Property
    
            Public Sub New(ByVal displayMemberPropertyName As String)
                Me.dispMemb = displayMemberPropertyName
            End Sub
        End Class

Implementing the Editor

Now, we're ready to implement the editor itself. We'll set the style to DropDown:

    Public Overloads Overrides Function GetEditStyle(ByVal context As _
                    ITypeDescriptorContext) As UITypeEditorEditStyle
        If Not context Is Nothing AndAlso Not context.Instance Is Nothing Then
            Return UITypeEditorEditStyle.DropDown
        End If
        Return UITypeEditorEditStyle.None
    End Function

EditValue and small service functions:

    Private edSvc As IWindowsFormsEditorService
    Private valMemb As ValueMemberAttribute

    <RefreshProperties(RefreshProperties.All)> _
    Public Overloads Overrides Function EditValue( _
                ByVal context As ITypeDescriptorContext, _
                ByVal provider As System.IServiceProvider, _
                ByVal value As [Object]) As [Object]
        If context Is Nothing OrElse provider Is Nothing _
                OrElse context.Instance Is Nothing Then
            Return MyBase.EditValue(provider, value)
        End If
        Dim att As SourceCollectionAttribute = _
                context.PropertyDescriptor.Attributes( _
                GetType(SourceCollectionAttribute))
        If att Is Nothing Then
            ' nothing we can do here. Let the default editor handle it

            Return MyBase.EditValue(provider, value)
        End If
        Me.edSvc = provider.GetService(GetType(IWindowsFormsEditorService))
        If Me.edSvc Is Nothing Then
            ' nothing we can do here either

            Return MyBase.EditValue(provider, value)
        End If

        ' prepare the listbox

        Dim lst As New ListBox
        Me.PrepareListBox(lst, att, context)
        If Me.valMemb Is Nothing Then
            lst.SelectedItem = value
        Else
            Me.valMemb.SelectByValue(lst, value)
        End If
        Me.edSvc.DropDownControl(lst)

        ' we're back

        If lst.SelectedItem Is Nothing Then
            value = Nothing ' nothing selected - nothing to return

        ElseIf Me.valMemb Is Nothing Then
            value = lst.SelectedItem ' return the item itself

        Else ' return a field from the selected item

            value = Me.valMemb.GetValue(lst.SelectedItem)
        End If
        Return value
    End Function

    Private Sub PrepareListBox(ByVal lst As ListBox, _
                ByVal att As SourceCollectionAttribute, _
                ByVal context As ITypeDescriptorContext)
        lst.IntegralHeight = True ' resize to avoid partial items

        Dim coll As ICollection = att.Collection(context.Instance)
        If lst.ItemHeight > 0 Then
            If Not coll Is Nothing AndAlso _
                lst.Height / lst.ItemHeight < coll.Count Then
                ' try to keep the listbox small but sufficient

                Dim adjHei As Integer = coll.Count * lst.ItemHeight
                If adjHei > 200 Then adjHei = 200
                lst.Height = adjHei
            End If
        Else ' safeguard, although it shouldn't happen

            lst.Height = 200
        End If
        lst.Sorted = True ' present in alphabetical order

        FillListBoxFromCollection(lst, coll)
        Me.AssignValueMember(lst, context.PropertyDescriptor)
        Me.AssignDisplayMember(lst, context.PropertyDescriptor)
        ' attach event handler

        AddHandler lst.SelectedIndexChanged, AddressOf Me.handleSelection
    End Sub

    Private Sub AssignValueMember(ByVal lc As ListControl, _
                ByVal pd As PropertyDescriptor)
        Me.valMemb = pd.Attributes(GetType(ValueMemberAttribute))
        If Me.valMemb Is Nothing Then Return
        ' maybe one day it'll work by itself...

        lc.ValueMember = Me.valMemb.ValuePropertyName
    End Sub

    Private Sub AssignDisplayMember(ByVal lc As ListControl, _
                ByVal pd As PropertyDescriptor)
        Dim att As DisplayMemberAttribute = pd.Attributes( _
                GetType(DisplayMemberAttribute))
        If att Is Nothing Then Return
        lc.DisplayMember = att.DisplayPropertyName
    End Sub

    Private Sub handleSelection(ByVal sender As Object, ByVal e As EventArgs)
        If Me.edSvc Is Nothing Then Return
        Me.edSvc.CloseDropDown() ' simply return to the EditValue procedure

    End Sub

Ready to go.

Using the code

To use the class, bind it to your property like this:

<Editor(GetType(UniversalDropdownEditor), GetType(UITypeEditor)), _
    SourceCollection("MyMethods"), _
    ValueMember("Name"), DisplayMember("Name")> _
Property SelectedMethodName() As String
' ...

<Browsable(False)> _
Property MyMethods() As System.Reflection.MethodInfo()

Note that if you have a special property created to serve as a source collection, you might want to conceal it by setting the Browsable attribute to False.

That's it. Enjoy!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here