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).