
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)
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
Return MyBase.EditValue(provider, value)
End If
Me.edSvc = provider.GetService(GetType(IWindowsFormsEditorService))
If Me.edSvc Is Nothing Then
Return MyBase.EditValue(provider, value)
End If
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)
If lst.SelectedItem Is Nothing Then
value = Nothing
ElseIf Me.valMemb Is Nothing Then
value = lst.SelectedItem
Else
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
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
Dim adjHei As Integer = coll.Count * lst.ItemHeight
If adjHei > 200 Then adjHei = 200
lst.Height = adjHei
End If
Else
lst.Height = 200
End If
lst.Sorted = True
FillListBoxFromCollection(lst, coll)
Me.AssignValueMember(lst, context.PropertyDescriptor)
Me.AssignDisplayMember(lst, context.PropertyDescriptor)
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
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()
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!