|

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!
| You must Sign In to use this message board. |
|
| | Msgs 1 to 15 of 15 (Total in Forum: 15) (Refresh) | FirstPrevNext |
|
 |
|
|
This is an awesome demo and very well written article. Thanks a bunch!
I noticed one annoying quirk, though. I brought the class file into my own library project, rather than compiling and referenceing it. This works fine, but when I would recompile my library project as part of the larger solution, it would crash VS when I tried the drop-down again. I traced this to the calls to "Return MyBase.EditValue(provider, value)". These caused infinite recursions and, I guess, stack overflows. I replaced those with "Return value", which stopped the crashes, but didn't achieve the goal of a useful drop-down on recompiles.
I discovered that the root cause of the problem was the call to "context.PropertyDescriptor.Attributes(GetType(SourceCollectionAttribute))". On subsequent compiles, this fails because the SourceCollectionAttribute type is now in another DLL than the one loaded by VS. So even though there is still an attribute that is of a type that has that same name, it's technically a different type, so it's never found.
I found a good solution, though. I cycle through context.PropertyDescriptor.Attributes looking for a type named "SourceCollectionAttribute", which is now guaranteed to get me the source collection. That means, though, that the place the variable is used -- PrepareListBox() -- needs to use Reflection to extract the collection. Here's a code snippet for doing so:
Imports System.Reflection ...
Dim Prop As PropertyInfo Dim Coll As ICollection Dim Args() As Object Prop = att.GetType().GetProperty("Collection") ReDim Args(0) Args(0) = context.Instance Coll = Prop.GetValue(att, Args)
This now works for me and guarantees the code that implments my custom SourceCollection attribute runs every time, even after recompile, and even though the UniversalDropdownEditor class' DLL is versions behind.
Cheers.
- Jim
-- modified at 17:34 Tuesday 4th April, 2006
|
| Sign In·View Thread·PermaLink | 5.00/5 (1 vote) |
|
|
|
 |
|
|
Hi Jim,
You are welcome.
Thanks a lot for the workaround, I'll look at it and modify the article when I have a bit more time.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
First of all, thank you for sharing your great knowledge. Now, the bugs that infect my mind:
I'm a great lover of NESTED properties (yes, like a treeview). I started with a project found HERE and then evolved it into my "DEFINITIVE WAY to design properties". Now, you must knoe that I also love Custom DropDowns.
And here comes the crash, since I'm trying to mix up these techniques.
I succeeded in designing my dropdowns for FIRST LEVEL properties (although I also have a couple of multiple properties under another one that contains them). But they don't share any editor nor property converters (they are always converted to and from "semicolon separated" strings, because they are containers for multiple properties). My DropDowns usually deal with several properties at once, non only one.
I also succeeded in having two of them (even more - if it works with two, it works with any number) on the FIRST LEVEL, sharing the same DropDown (and property converters, too). But I'm breaking my head to have one of them on the first level and another one (of the same type) on the second, or third or n th level.
I really can't go any further, it's four days I'm trying, and I'm quite frustrated...
Any help will be really precioous.
Thank you a lot
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi Klaus!
You are welcome.
I'm glad you made use of my code and even made your own enhancements to it.
I wonder if by second, third, etc. level you mean properties in embedded objects.
If it is so, then to make them editable as well, simply inherit the types of your embedded objects from Component. Once you do that, a contract or expand option will appear in the property grid, and you'll be able to accomplish the same operations as on the first level properties.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Me again. I would like to convert this to C#. Before I do that, I really need to recompile the code with Option Strict enabled. It's a piece of cake for the most part, using the CType() function fixes most of the compile errors....except for one line of code.
In SelectByValue() you compare the value of two variables of type Object. However, with option Strict you must compare them as their actual type. I haven't been able to come up with a valid piece of code to do this. Any ideas?
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
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!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hmm... That's strange.
I understand that this line of code is troublesome, right?
If Me.GetValue(item) = val...
If it is, you can substitute it with
If Object.Equals(Me.GetValue(item), val)...
It does exactly the same (checking for null references, too).
Also, you might try to use CObj function on each one of the values.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Duh! I don't know how I misread that the first time. Somehow I had thought that function compared whether the instance of the object was the same instance as the other. But, the derived class would, of course, override that and do something more useful. Thanks for pointing it out. I must have been in "friday afternoon ready to go home" mode.
This type editor class is a handy, very cool piece of code, by the way. It's not too easy to find good, clear information about this part of Visual Studio. Thanks for it.
Email me if you want a copy of the example converted to C-Sharp. I'll finish it up next week.
--JV
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Thank you for the kind words about the code.
I use VB.NET rather than C# for .NET programming, but if you want to, you can post a "follow-up" article here, too. Good for your resume as well .
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
It's a type editor. It doesn't matter where you use it.
When you're working in design mode, it's always Windows.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Ok, I wondered about that. Thanks for clearing it up. The reason for confusion I've seen examples which make use of editor objects in System.Web.UI.Design namespace, so it made me wonder if there were separate namespaces for these things, maybe the property sheets weren't identical.
After all, the datagrid between web and windows are quite different. Ya never know.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Oh, that's valid point .
System.Web.UI.Design contains auxiliary classes to handle web-specific controls... but still they're invoked via Windows PropertyGrid control.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Greetings!
I downloaded and built the dropdown editor and its demo project. I ran the demo project. I saw a form get created with a couple of text boxes filled in with the dropdown editor.
But what if I want to invoke a custom editor at design time? I want to create a control that has a public property that can be filled in by selecting a choice from a list. I want to drop the control on the form, select the control, find the property in the property browser, click the down arrow, and have your dropdown editor supply the list of possible choices. How do I set that up?
Thanks very much!
Rob Richardson
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi Rob,
Using the editor in design time was its primary purpose; it's just it was easier to demonstrate it in runtime. Those text boxes were part of a PropertyGrid control which you normally encounter in designers.
Using the editor is described in the chapter Using the Code. Simply put, you must specify the SourceCollection attribute over your property passing the name of the source property (the one which lists the choices).
If you want to customize the display (rather than rely on a collection entry's ToString method) and the returned values of your collection, use DisplayMember and ValueMember attributes respectively.
You're welcome .
Best regards, Vadim Berman
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
General News Question Answer Joke Rant Admin
|