Introduction
.NET includes a number of different UITypeEditor
s such the StringCollectionEditor
used to edit Combo
and Listbox
initial contents or any property defined as a Collection(of String)
. There is also a nice ControlsCollectionEditor
which allows the user to add new controls to your Component. This article will show you how to implement a UITypeEditor
which allows you to select from controls already on the form.
Background
It has always been more than a little mysterious how to get a list of existing form controls for a UITypeEditor
(which many refer to in shorthand as a UIDesigner
). Mike-MadBadger worked out the hard part for this in his article 'Accessing the Controls on a Form at Design Time'. It is an excellent tip dealing in some detail the basics of a UITypeEditor
works. I will not be re-hashing those details here as it is a short, well written article.
As he points out, he drew heavily from a 2007 article by Saeed Serpooshan. Saeed's article is more in depth but an excellent UIDesigner
primer. Mike took Saeed's work showing how to implement a UITypeEditor
, made it a set of generic procedures in an abstract
(MustInherit
in VB) class and then replaced the standard drop down editor with a very nice Dialog Form.
The two pieces make an excellent starting point for a highly reusable ControlsCollectionEditor
, which is what I will present here.
The Issues
First, this version is intended for use only by Components. For instance, you are writing an ExtenderProvider
(like ErrorProvider
or ToolTip
) which inserts a new property onto various controls. It is not intended for use by Container Controls, for instance if your project inherits from Panel
, in this case you should be able to just drag/drop the desired controls.
One major issue is that certain common controls seem to make their way into the form's controls collection even though they are components. The UIDesigner
actually has access to the components on the form which seems confusing. Since the UIDesigner
is attached to a property defined as Collection(Of Control)
why are there Components
in it? Well, .NET doesn't actually pass the backing field or Collection to the Designer Code. Mike got the form's Components
through some properties associated with the instance passed to the Designer. As such, all that is available is a list of your container's components (context.Container.Components
).
This is great, but trying to add Components
to a Collection(Of Control)
will end badly. One thing I immediately encountered was the DataGridViewColumn
. Since it will only work with/attach to a DataGridView
it is unlikely anyone actually wants to work with it in this context, and second, it is actually a component, not a control.
Another possibility, is the TabPage
. It is possible that someone might be developing a component to work with these, but they are also a specialty control which can only be added to a TabControl
. And there are things like the TableLayoutPanel
- since this is invisible, it seems highly likely that no one will ever want it in their list of controls. So there are a great many more cases where we would want to exclude different types of controls from our Collection(Of Control)
.
In the course of developing an UnDoManager
component, I set out to again use the Mike-Saeed ControlsCollectionEditor
to allow the developer to select the controls to be managed. Since not all controls interact with the user (like Labels
and GroupBox
), they are not supported. A final issue is that the parent form is included in the list. That might be fine in some cases, but not mine. So a means to filter controls was needed. As this was the 3rd time I would rework the class, I decided to fix it for good.
So among other things, I added a selection or filtering mechanism to their work so the developer can either select the control types allowed or exclude certain types.
Implementation
Inherit Everything
Mike reworked Saaed's class to act as an abstract
/MustInherit
class, then built his ControlsCollectionUIEditor
on it. It works great as a generic demo, but as shown, in the real world things are often more complex. So, I did the same thing as Mike: I made some small changes to his class and made it MustInherit
and will show you how to use it as a Base Class. This will require very little code to implement.
No Forms Allowed
First, a flag was added to ExcludeForm
(s) from the control list. Since Forms inherit from Control, they look like just another control and can make it onto the list of controls. The exclusion mechanism (described next) could be used for forms as easily as a TableLayoutPanel
, but I also used a ExcludeForm
flag as a convenience for those who just need to exclude the form. Keep in mind that this is for working with a component you are developing, so you should know in detail what it can and cannot deal with.
An Exclusive Club
There was an inherent need to exclude certain things like the DataGridViewColumn
(a component, not a control), the TableLayoutPanel
(invisible) and maybe the TabPage
(can only be added to a TabControl
). So, an exclusion mechanism was clearly needed.
The standard .NET ControlsCollection
editor has an inclusion mechanism and that's what I needed for the UnDoManager
: a way to only include the types of controls my tool was designed to work with. For maximum flexibility, I implemented it both ways: An include List
and an exclude List
. This way, you use either one depending on whether you need to let in, or keep out just a few types.
Just before Mike's dialog form displays, Saaed's base class will call MustOverride
LoadValues
. Here is how the filters are applied:
Dim bAdd As Boolean = True
Dim thisCtl As Control = Nothing
For Each obj As Object In context.Container.Components
bAdd = True
If TypeOf obj Is Control Then
thisCtl = CType(obj, Control)
If ExcludeForm Then
bAdd = Not (TypeOf thisCtl Is Form)
End If
If (typeIncludeOnly IsNot Nothing) AndAlso (typeIncludeOnly.Count > 0) Then
If typeIncludeOnly.Contains(thisCtl.GetType) = False Then
bAdd = False
End If
End If
If (typeExclude IsNot Nothing) AndAlso (typeExclude.Count > 0) Then
If typeExclude.Contains(thisCtl.GetType) Then
bAdd = False
End If
End If
If bAdd Then
myCtl.Items.Add(thisCtl)
End If
End If
Next
These various settings I've added are just Protected Friend
variables. The lists are already instanced, so there is nothing for you to do except add your Types to them. Code such as:
If (typeExclude IsNot Nothing) Then
tries to watch for someone who thinks it is a good idea to set it to Nothing just in case there is something in it (there are no defaults). Otherwise, I left Try/Catch
blocks out of it because this is a design time tool only, and if you are using it wrong it seems best to let the Exceptions through so you know. Catching them in a design time tool such as this actually makes them harder to find (there was one in Saaed's code which was very hard to find).
Other Small Changes
One of the things I liked best about Mike's work was learning how to use a modal dialog form instead of the default dropdowns. However, for maximum flexibility, I implemented a second class to use the dropdown method. These are small and terribly ugly with .NET appending the full type name and other "useful" information to the Control Name. That said, there are cases where that method might be more appealing.
The result is that there are now two classes: ControlCollectionDropDownUIEditor
for invoking the dropdown CheckboxList
and ControlCollectionDialogUIEditor
for the Dialog Form version. There are other classes, but they are MustInherit
base classes.
Most other changes were to the structure, such as making it into a DLL to prevent loosing files or accidentally changing something critical. To use the DLL form, add a reference and import the namespace. If you do choose the file method, the dialog form was merged into the ControlCollectionEditor.vb file so there is one less file to keep track of.
What it Looks Like
Selecting the ellipsis for the controls property displays the Dialog Form. In this case, we exclude Labels
and GroupBox
from the eligible list.
The .NET default DropDown
version follows the same rules.
Using the Code
Using the included demo, here is what is required to implement the ControlCollectionUIEditor
. (the demo doesn't DO anything but provide a host for an ExampleComponent
to use in VS). You will very likely need to Clean and Build the project since VS needs a compiled version of the project to implement the ExampleComponent
in the project.
1. Decorate the property which will be implementing a Collection(Of control) with the EditorAttribute:
Private _TargetControls As New Collection(Of Control)
<EditorAttribute(GetType(ExampleFormControlCollectionUIEditor), _
GetType(System.Drawing.Design.UITypeEditor))> _
<DesignerSerializationVisibility(DesignerSerializationVisibility.Content)> _
Public Property TargetControls() As Collection(Of Control)
- Note that
Collection(Of Control)
is from System.Collections.ObjectModel
, not the VisualBasic.Collection
.
- The designer name is one you create for your
Class
property: in this case, ExampleFormControlCollectionUIEditor
.
- This has nothing to do with the
UIEditor
, but you will also need these procedures for your Component
to work correctly:
Public Sub ResetTargetControls()
_TgtControls = Nothing
End Sub
Public Function ShouldSerializeTargetControls() As Boolean
Return (_TgtControls IsNot Nothing)
End Function
Note how your property name is embedded into the procedure names.
2. Write the UIEditor
Fully 99% of the work is already done and should be in the DLL, you just need to provide a local class. This class can be in the same file as your main project (your property must be decorated as above):
Imports Plutonix.UIDesign
<System.Security.Permissions.PermissionSetAttribute(_
System.Security.Permissions.SecurityAction.Demand, Name:="FullTrust")> _
Public Class ExampleFormControlCollectionUIEditor
Inherits ControlCollectionDialogUIEditor
Public Sub New()
MyBase.new()
MyBase.ExcludeForm = True
typeIncludeOnly.Add(GetType(TextBox))
typeIncludeOnly.Add(GetType(ComboBox))
typeExclude.Add(GetType(TabControl))
End Sub
End Class
Note: It is hard to imagine a case using both the inclusion and exclusion list. Use whichever one fits your use case best. Both are shown above to illustrate the names and usage.
Notes:
- The class name (
ExampleFormControlCollectionUIEditor
) is exactly the same one used in the property EditorAttribute
.
- Both
typeIncludeOnly
and typeExcludeOnly
are a List(Of System.Type)
- There are NO DEFAULT entries for either one. I debated adding
TableLayoutPanel
and maybe TabPage
as default exclusions, but decided that was bad since you can't easily see what is in there. Also, having used this 4 times now, it seems including only certain types is by far the more common use case.
- If you leave the
typeIncludeOnly
empty (Count == 0
), all the controls on the form will show in the list. When Types are added, this sort of works like the CanExtend
function in an ExtenderProvider
.
- As noted, the form itself can be excluded either using the
ExcludeForm
flag or by adding Form
to the typeExcludeList
. The flag is kind of nice if that is all you need to modify as to controls.
- If you do not need to tweak any settings, you will just need to call
MyBase.New
To test this, you must clean and build the demo. Then in design mode, open the form, select the component in the form tray, then in the Properties window, select 'TargetContols
'. That's it. Saeed's work takes over to implement the .NET collection editor using Mike's Dialog Form.
The demo has an example component and BOTH the Dialog
and DropDown
versions coded. To test the DropDown
version:
- Change the
EditorAttribute
on your property to ExampleDropDownControlCollectionUIEditor
- Clean and build so VS can compile and use the other editor in the Properties window
Class, Member Reference
ControlCollectionDialogUIEditor MustInherit Class
Allows the developer to select existing Form Controls using a modal dialog. Uses a CheckedListBox
so multiple controls can be selected.
ControlCollectionDropDownUIEditor MustInherit Class
Provides a DropDown
CheckedListBox
to allow the developer to select multiple Form Controls.
typeIncludeOnly List(Of System.Type)
A list of System.Types
(controls). Only controls of the Types in the list will show on the UIEditor
.
typeExclude List(Of System.Type)
A list of System.Types
(controls). All controls of these Types will be excluded from the Dialog
or DropDown UIEditor
.
ExcludeForm Boolean
Flag indicating whether to include this component's parent form in the list. Default is True
.
CheckControlWidth Integer
Applies only to the ControlCollectionDropDownUIEditor
. Sets the width of the drop down. The minimum is 280, default value is 400.
Addendum
In addition to recompiling to make a NET 4.0 version, the update includes a smart UIEnumEditor
. This will automatically use the right control for flag/bitwise Enum
properties, and detect and use descriptions if present.
.NET natively handles properties declared as Enum
s quite well - unless it is a bitwise or flag type Enum
:
<Flags>
Public Enum FlagColors
None = 0
Red = 1
White = 2
Blue = 4
Green = 8
Yellow = 16
End Enum
The default NET UIEditor
seems to ignore or be unaware of the FlagsAttribute and uses a single select ListBox
in the Properties window, which does not allow multiples to be selected as they should. The UIEnumEditor
provides a dropdown CheckedListBox
for selecting multiple items:
<Editor(GetType(UIEnumEditor), GetType(UITypeEditor))>
Public Property EnumOfFooExample As Foo
I routinely associate descriptions with Enum
s when they will be displayed to the use in a Combo or List box, and sometimes I like these descriptions to be used in the IDE Property panel.
Public Enum Stooges
<Description("Larry - Funny one")> Larry
<Description("Moe - 'Smart' One")> Moe
<Description("Curly - Sore One")> Curly
<Description("Shemp - One with bad haircut")> Shemp
<Description("CurlyJoe - Last one")> CurlyJoe
End Enum
<Editor(GetType(UIEnumEditor), GetType(UITypeEditor))>
Public Property EnumOfStooge As Stooges
However, I don't like having to have specify a different UIEditor
based on the nature of the underlying Enum. This UIEnumEditor
is intended to be smart and take the appropriate action based on the Enum
property:
Simple Enums
- Non-Flag/bitwise
Enum
s will automatically use the Description
text if available.
- Any members missing a
Description
will use the Enum
name.
- A simple
Enum
property with no descriptions will look/act the same as the default .NET.
Bitwise Enums
- Flag/bitwise
Enum
s, will automatically use a CheckedListBox
dropdown with the Enum
Names.
- To work properly, the
Enum
needs the FlagsAttribute
and the values must actually be bitwise values (0, 1, 2, 4, 8, 16, 32, 64 ...)
- These will never show the Zero value member. This value should represent None, which is indicated by selecting no members.
You can subclass the editor to change the default behavior. For instance, to tell it to display the Descriptions
for a flag Enum
property:
Public Class EnumFlagFruitEditor
Inherits UIEnumEditor
Public Sub New()
MyBase.New()
Me.UseDescription = True Me.ControlWidth = 280
End Sub
End Class
Since the design time properties are used by developers, not end users, it seems best (to me) to use the Enum
Names for bitwise properties to make clear the combination being created. So the default behavior is not to use descriptions with these. To override this, set the UseDescription
property to True
.
Likewise, you can force simple/non bitwise Enums
to ignore the description by subclassing and setting UseDescription
to false
(but this is the default NET behavior).
Both controls are Sizeable
, but you can set the initial dropdown width using ControlWidth
.
The demo illustrates several combinations of Flag and non-Flag Enum
properties. However, it is difficult to tell what is being illustrated without looking at the code (see EnumCtl.vb). The demo uses the same default UIEnumEditor
for both normal and flag style Enums
both with and without descriptions associated with them.
Summary
Developing tools which will run in the developer's VS designer can be confusing: you are after all using Visual Studio to write something which will run in VS at design time. It is not something many of us do everyday and there are not a lot of clear references on the subject.
Saeed's article on this is informative and can be very helpful in understanding some of the arcane aspects of UI Designers in general. Mike's article is equally valuable, abstracting Saaed's work into a set of base tools and adding a dialog form to the toolset.
The object of the code provided here was to use these previous efforts to provide the means to implement a flexible and controls collection editor powerful enough to handle a variety of situations.
History
- 2013-11-25
- Initial article and ver 1.02 of the sample code
- 2014-05-28
- Updated .NET 4 version
- Added a small
UIEnumEditor
- Addendum explaining the same