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

VB.NET runtime control designer for windows forms

0.00/5 (No votes)
28 Jun 2015 1  
Simply add a single code module to add instant design capabilities to all windows forms of your application!

Introduction

Sometimes it is necessary to allow the users of your application to change the design of one or more forms. The requirements to solve these functionalities are complex and need a lot of time and code for each form to add this feature. Searching the web for solutions you'll find only some rudimental examples or ready-to-use components with all desired features - but none of them is for free.

Background

The demo project contains a single and reusable module with 2 major classes that can be used in any application using windows forms. To activate the designer on any form you only have to create an instance of the control designer class, passing the desired form and some more simple parameters (most of them are optional) and your form is in design mode. Disposing the class will end the design mode - that's as simple as you expected it to be, isn't it?

Of course there is no real need for that if we aren't able to save and restore the changes, the user made. Therefore we'll find a second class (the control manager) that provides these methods. Add a call to the load method in your form's load event and a call to the save method in the form's closing event and you're done!

The designer allows single or multiple control selection, moving and resizing with mouse or arrow keys, alignment of multiple controls to left, right, top or bottom, snap-to-grid/snap-to-container-border features and editing of the most common display properties of the form and controls within the same property grid, you're using at design time in Visual Studio.

Both, the ControlDesigner and the Save method of the ControlManager allow to pass a simple List of controls that you want to exclude from design.

Using the code

1) Add the file ControlDesigner.vb to your VB.NET project.

2) For each form you want to enhance with designer capabilities:
    a) add the following code to the form's load event:

Dim objCM As New ControlManager

objCM.RestoreProperties(Me)

objCM.Dispose()

     b) add the following code to the form's closing event (except designer button from save):

Dim objCM As New ControlManager

objCM.SaveProperties(Me, New List(Of Control)({btnDesigner}))

objCM.Dispose()

       c) add a boolean flag at form level to reflect design mode:

Private bolDesignMode As Boolean = False

       c) add the following code to the button (or menuitem) event, the user starts/stops design mode with:

Select Case btnDesigner.Text
    Case "Designer"
         btnDesigner.Text = "Done"
         bolDesignMode = True
         objCD = New ControlDesigner(Me, New List(Of Control)({btnDesigner}), Color.LightYellow)
    Case Else
         objCD.Dispose()
         bolDesignMode = False
         btnDesigner.Text = "Designer"
End Select

        (you may specify custom values for the optional parameters to adapt the designer to your application needs)
    
    d) To prevent the controls from performing their standard action on click (or other) event, while in design mode,
       extend all these event procedures to perform only, if not in design mode. For example:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    If Not bolDesignMode Then
       'Do something...
    End If
End Sub

3) Run your application and test the design features. Right-click on 2 or more controls with Shift key down to align multiple controls:

           
            
...or right-click the form or any control(s) to edit their properties:

           

You don't need to provide the context menu and the properties edit dialog within you application - both are created 'on-the-fly' out of the module.

Stepping into details

Besides the usual event hooking for drag/drop controls there are 2 major wrapper classes that allow to edit the most common display properties of any control in a property grid and to de-/serialize these properties from/to a XML file:

1) Control or Form wrapper (classes PropertyWrapper and FormPropertiesWrapper)
The great advantage of a Propertygrid is the ability to simply assign any object to it's SelectedObject property and the Propertygrid will do all the work for us as to analyze the object's methods, properties and events, organize them in categories and create all the editors for each datatype as well as showing the associated help text for each of them. Unfortunately there is no simple method to limit the displayed grid entries to exactly what you want to allow the user for editing. The wrapper classes will do this for you: they expose only those properties we want to be displayed/edited. To support the complete features of the PropertyGrid, we have to prefix each property procedure with a help text and a category information. For example the property for a Backcolor would look like this:

<DescriptionAttribute("Defines the control's backcolor."),
CategoryAttribute("Display")> _
Public Property BackColor() As Color
    Get
        Return _lstControls.Item(0).BackColor
    End Get
    Set(ByVal Value As Color)
        For Each ctl As Control In _lstControls
            ctl.BackColor = Value
        Next
    End Set
End Property

Where _lstControls is a List(Of Control) containing all the currently selected controls. As you can see, the Get method always returns the value of the first control within the List of selected controls where the Set method distributes the new property value to all currently selected controls.
The method prefix DescriptionAttribute contains the help text, we want to be displayed along with this property and the CategoryAttribute contains the display name of the category, we want this property to be grouped in. You may reduce or extend the list of properties you want the user to edit but keep in mind, that the control wrapper is used for all controls and you have to handle the problem, that not all controls provide all properties. Also you have to reduce or extend the following serialization wrapper class the same way to ensure, that the control manager supports the same properties on save and restore.

2) The serialization wrapper (class ControlInfo)
The next problem that we'll face is when we want to save the supported properties of any control to a XML file. Allright: the .NET framework provides easy to use methods and objects to simply serialize and deserialize objects but...if you try to pass - for example - a List object of the above control wrapper to these methods you'll get an error saying that the serializer failed to reflect some of the properties. In detail these are all properties using complex datatypes like Location, Size, Margin, Padding and Font that are structures with 2 or more values. The Location and Size structures consists of 2 values: Left and Top resp. Width and Height, the Margin and Padding properties consists of 4 values and the Font property has even more! Properties as Fore- and Backcolor are using a datatype Color that can't be translated automatically and also enumerations like Anchor and Dock may cause problems. To solve this, you can think of the serialization wrapper as a converter with a 2 sides interface:
- one with all property datatypes as we are used to deal with in code
- another one that dissolves these properties to datatypes that can be handled on de-/serialization.
The only trick is to tell the de-/serializer which to use and which to ignore when doing it's work. Let's see 2 examples:

a) Converting a Color property
The standard interface will look like this:

<Xml.Serialization.XmlIgnore> _
Public Property BackColor() As Color
    Get
        Return _BackColor
    End Get
    Set(ByVal Value As Color)
        _BackColor = Value
    End Set
End Property

The method prefix XmlIgnore will tell the de-/serialzer to ignore this property while it is still available with the dataype Color for directly exchange this value with a control. For the de-/serializer we provide the same value in HTML notation:

<Xml.Serialization.XmlElement("BackColor")> _
Public Property BackColorXML() As String
    Get
        Return ColorTranslator.ToHtml(_BackColor)
    End Get
    Set(ByVal Value As String)
        _BackColor = ColorTranslator.FromHtml(Value)
    End Set
End Property

The method prefix XmlElement tells the de-/serializer to handle this property name as 'BackColor' (and not as 'BackColorXML'!). Using the ColorTranslator we easily convert the color value from and to HTML notation. The class-internal variable _BackColor always holds the color value as datatype of Color.

b) Converting a complex datatype (structure) with several sub-values
Datatypes like Location, Size, Marging, Padding or Font consists of several sub values. The simpliest way to save these properties is to dissolve them to several single properties that will be combined again when the complex datatype will be requested. For example the standard Location property will look like this:

<Xml.Serialization.XmlIgnore> _
Public Property Location() As System.Drawing.Point
    Get
        Return New Point(_Left, _Top)
    End Get
    Set(ByVal Value As System.Drawing.Point)
        _Left = Value.X
        _Top = Value.Y
    End Set
End Property

Again we use the method prefix XmlIgnore to hide this property for de-/serialization. The Get method combines the internal _Left an _Top values to a new Point structure that will be returned while the Set method dissolves the Point structure to 2 single values _Left an _Top. Therefore the XML de-/serializer will find 2 properties instead:

Public Property Left() As Integer
    Get
        Return _Left
    End Get
    Set(ByVal Value As Integer)
        _Left = Value
    End Set
End Property

and

Public Property Top() As Integer
    Get
        Return _Top
    End Get
    Set(ByVal Value As Integer)
        _Top = Value
    End Set
End Property

We don't need to use the prefix XmlElement here, because there are no properties with the same name for the standard interface.

The same way we will dissolve all complex datatypes like Size, Margin, Padding and Font.

Enumerations normally are representing an Integer value, therefore you simply declare those properties as Integer instead of their enumeration name - the implicit conversion capabilities of VB.NETwill do the rest for you.

...and don't forget to prefix the whole wrapper class with the prefix <System.Serializable> otherwise de-/serialization won't work!

You'll find a more detailed description of all features and limitations in the header, classes and procedures of the code module. Feel free to extend the module with own or better features - if you'd like to share your extensions with others I'd be glad to add them to this article and demo project (of course your name will be shown on all your changes and extensions!).

History

2015-06-28
First release.

2015-07-01
Both procedures of SelectControl extended that way, the user hasn't to keep the shift key down while working with multiple selected controls. Changed line

If Not My.Computer.Keyboard.ShiftKeyDown Then

to

If (Not My.Computer.Keyboard.ShiftKeyDown) AndAlso (Not lstSelectedControls.Contains(sender)) Then

(selection will not be cleared if clicked control is already part of current selection).

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