Click here to Skip to main content
15,881,248 members
Articles / Programming Languages / Visual Basic
Article

Handy Type Editors. Universal Dropdown Editor

Rate me:
Please Sign up or sign in to vote.
4.17/5 (13 votes)
5 May 20042 min read 85.6K   540   32   16
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).

VB
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:
    VB
    <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:
    VB
    <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:
    VB
    <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:

VB
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:

VB
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:

VB
<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



Comments and Discussions

 
Generalmulti selection drop down box Pin
Member 228177124-Aug-10 11:25
Member 228177124-Aug-10 11:25 
GeneralVS crashes: DLL version issue Pin
Jim Carnicelli4-Apr-06 11:29
Jim Carnicelli4-Apr-06 11:29 
AnswerRe: VS crashes: DLL version issue Pin
VadimBerman4-Apr-06 12:49
VadimBerman4-Apr-06 12:49 
GeneralThis is really GREAT. But, there are sone issues... Pin
Luca Crisi, MCP5-Aug-05 4:05
Luca Crisi, MCP5-Aug-05 4:05 
GeneralRe: This is really GREAT. But, there are sone issues... Pin
VadimBerman7-Aug-05 1:35
VadimBerman7-Aug-05 1:35 
GeneralType Safety / C# Pin
JVMFX25-Feb-05 10:27
JVMFX25-Feb-05 10:27 
GeneralRe: Type Safety / C# Pin
JVMFX25-Feb-05 10:29
JVMFX25-Feb-05 10:29 
P.S. Hey, you VB.NET programmers: You should always enable Option Strict!!!! The only exception is if you have to port some legacy VB code to DotNet. Otherwise...cmon guys! Do it typesafe!Cool | :cool:
GeneralRe: Type Safety / C# Pin
VadimBerman25-Feb-05 22:32
VadimBerman25-Feb-05 22:32 
GeneralRe: Type Safety / C# Pin
JVMFX26-Feb-05 4:30
JVMFX26-Feb-05 4:30 
GeneralRe: Type Safety / C# Pin
VadimBerman27-Feb-05 21:22
VadimBerman27-Feb-05 21:22 
QuestionWhat about ASP.NET? Pin
JVMFX24-Feb-05 8:05
JVMFX24-Feb-05 8:05 
AnswerRe: What about ASP.NET? Pin
VadimBerman24-Feb-05 8:47
VadimBerman24-Feb-05 8:47 
GeneralRe: What about ASP.NET? Pin
JVMFX24-Feb-05 13:02
JVMFX24-Feb-05 13:02 
GeneralRe: What about ASP.NET? Pin
VadimBerman25-Feb-05 2:17
VadimBerman25-Feb-05 2:17 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.