Handy Type Editors. Universal Dropdown Editor






4.17/5 (11 votes)
May 6, 2004
2 min read

86369

540
Implementing a universal dropdown type editor.
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 ofListBox
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 toGetValue
andSelectByValue
functions: we cannot useListControl.SelectedValue
since we're not binding the data throughListControl.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'sToString
method is used. All we must do is assign the string to theDisplayMember
property of theListBox
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!