Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Selecting Form's Controls at Design Time

0.00/5 (No votes)
2 Jun 2014 1  
Accessing the Form's Controls in your UIDesigner

Introduction

.NET includes a number of different UITypeEditors 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
   'Cycle through the components owned by the form in the designer
    bAdd = True

    ' exclude other components - this weeds out DataGridViewColumns which
    ' can only be used by a DataGridView
    If TypeOf obj Is Control Then
        thisCtl = CType(obj, Control)

        If ExcludeForm Then
            bAdd = Not (TypeOf thisCtl Is Form)
        End If

        ' custom Include only these list
        If (typeIncludeOnly IsNot Nothing) AndAlso (typeIncludeOnly.Count > 0) Then
             If typeIncludeOnly.Contains(thisCtl.GetType) = False Then
                 bAdd = False
             End If
        End If

        ' custom exclude list 
        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 provides the name of the Editor required
    <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:
' these control whether or not to serialize the control
   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     ' modified base class in the DLL
         
    Public Sub New()
        MyBase.new()
        MyBase.ExcludeForm = True    ' defaults to False
                   
        ' the sole types allowed in the list
        typeIncludeOnly.Add(GetType(TextBox))
        typeIncludeOnly.Add(GetType(ComboBox))
        ' ...
        ' 
        ' to list a only a few types to exclude
        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:

  1. Change the EditorAttribute on your property to ExampleDropDownControlCollectionUIEditor
  2. 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 Enums 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 Enums 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 Enums 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 Enums, 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       ' change the default
            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

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