This is a control I wrote as another experiment with Expandable Properties.





You can download the demo project: here



The Control itself only serves as a visual representation of the Expandable BorderColors Property, which has four sub-Properties, Bottom, Left, Right, + Top, all of type Color, which can all be set independently, and changing any of the sub-Properties is reflected in the display color of the corresponding border line on the control.











The Expandable Property - BorderColors - I've written so as to mimic a Color Property, which can be used in much the same way as a Color Property to set the color of all four border lines in one Property. I achieved this by utilizing a TypeEditor class as the UITypeEditor, which contains most of the code needed to give the Property very similar functionality to a Color Property.


Imports System.Drawing.Design
Imports System.Windows.Forms.Design
Imports System.Windows.Forms.PropertyGridInternal.PropertyGridCommands

Public Class TypeEditor1
    Inherits UITypeEditor

    Public Overrides Function GetEditStyle(ByVal context As System.ComponentModel.ITypeDescriptorContext) As System.Drawing.Design.UITypeEditorEditStyle
        Return UITypeEditorEditStyle.DropDown
    End Function

    Public Overrides Function EditValue(ByVal context As System.ComponentModel.ITypeDescriptorContext, ByVal provider As System.IServiceProvider, ByVal value As Object) As Object
        If provider IsNot Nothing Then

            Dim editorService As IWindowsFormsEditorService = TryCast(provider.GetService(GetType(IWindowsFormsEditorService)), IWindowsFormsEditorService)
            If editorService Is Nothing Then
                Return value
            End If

            ' This is the Color type editor - it displays the drop-down UI calling
            ' our IWindowsFormsEditorService implementation.
            Dim Editor As New ColorEditor

            ' Display the UI.
            Dim c As Color
            Dim NewValue As Object = Editor.EditValue(editorService, c)

            ' If the user didn't cancel the selection, return the new color.
            If Not CType(NewValue, Color) = Color.Empty Then
                Return New BorderColors(CType(NewValue, Color))
            End If

        End If

        Return value

    End Function

    Public Overrides Function GetPaintValueSupported(ByVal context As System.ComponentModel.ITypeDescriptorContext) As Boolean
        Return True
    End Function

    Public Overrides Sub PaintValue(ByVal e As System.Drawing.Design.PaintValueEventArgs)
        Dim img As New Bitmap(18, 14)
        Dim gr As Graphics = Graphics.FromImage(img)
        Dim parts() As String = e.Value.ToString.Split(","c)
        Dim c As Color = Nothing
        If parts.Length = 3 Then
            Dim values() As Integer = Array.ConvertAll(parts, Function(s) CInt(Val(s)))
            c = Color.FromArgb(values(0), values(1), values(2))
            gr.Clear(c)
        Else
            c = Color.FromName(e.Value.ToString)
            gr.Clear(c)
        End If

        e.Graphics.DrawImage(img, New Point(2, 2))

        MyBase.PaintValue(e)

    End Sub

End Class




As you can see, this class contains 3 Overridden Functions + 1 Overridden Sub-procedure:

  • GetEditStyle

                Here, I specified that the Property uses a DropDown UI Editor

  • EditValue

                This is the Function that displays + retrieves the value from, the (DropDown) ColorEditor

  • GetPaintValueSupported

                Here I returned True to specify that I would be supplying a custom Property image

  • PaintValue

                This is where I draw the small colored rectangle which appears to the left of the Color name.




As an alternative the BorderColors Property can be changed by typing a value in the Properties Window, for example, typing in 255,0,0 would result in the displayed Property showing a Red rectangle + the word 'Red'. You can also just type in a Color name + it'll be recognized (if it exists). This functionality is achieved by using a TypeConverter class, (ExpandableObjectConverter) which converts between String + BorderColors.




This is the borderColors class, which contains the four sub-Properties, Bottom, Left, Right, + Top.
Sub New has 2 Overloads, allowing one Color for all 4 Properties, or independent Colors. I've Overridden the ToString Function which is what is displayed in the Properties Window + contains the name of the Color you've chosen. I used a Function to return the correct Color name, as there is some overlapping in the Colors class + if you choose [SystemColors] WindowText, it'll display 'WindowText' + not 'Black' + vice versa.

The BorderColors Property will only display a value in the Properties Window if all 4 sub-Properties are the same.

The 4 Properties have Attributes, that specify what to do if the Property is changed.:


Imports System.Drawing
Imports System.ComponentModel

<TypeConverter(GetType(BorderColorsConverter))>
Public Class BorderColors
    ' Auto Properties. 
    <RefreshProperties(RefreshProperties.All), _
    NotifyParentPropertyAttribute(True)> _
    Public Property Top As Color
    <RefreshProperties(RefreshProperties.All), _
    NotifyParentPropertyAttribute(True)> _
    Public Property Right As Color
    <RefreshProperties(RefreshProperties.All), _
    NotifyParentPropertyAttribute(True)> _
    Public Property Bottom As Color
    <RefreshProperties(RefreshProperties.All), _
    NotifyParentPropertyAttribute(True)> _
    Public Property Left As Color

    Public Sub New(all As Color)
        Dim c As Color = GetColor(all)
        Left = c
        Top = c
        Right = c
        Bottom = c
    End Sub

    Public Sub New(cLeft As Color, cTop As Color, cRight As Color, cBottom As Color)
        Left = GetColor(cLeft)
        Top = GetColor(cTop)
        Right = GetColor(cRight)
        Bottom = GetColor(cBottom)
    End Sub

    Public Overrides Function ToString() As String
        If Left = Top AndAlso Left = Right AndAlso Left = Bottom Then
            Return GetColorName(Left)
        Else
            Return String.Empty
        End If
    End Function

    Private Function GetColorName(c As Color) As String
        If (c.IsSystemColor OrElse c.IsNamedColor OrElse c.IsKnownColor) And Not c.Name.Contains(",") Then Return c.Name
        Dim allColors = [Enum].GetNames(GetType(System.Drawing.KnownColor))
        For i = 0 To allColors.Count - 1
            Dim thisCol = Color.FromName(allColors(i))
            If (c.R = thisCol.R) AndAlso (c.G = thisCol.G) AndAlso (c.B = thisCol.B) Then
                Return thisCol.Name
            End If
        Next
        Return c.R & ", " & c.G & ", " & c.B
    End Function

    Private Function GetColor(c As Color) As Color
        If (c.IsSystemColor OrElse c.IsNamedColor OrElse c.IsKnownColor) And Not c.Name.Contains(",") Then Return c
        Dim allColors = [Enum].GetNames(GetType(System.Drawing.KnownColor))
        For i = 0 To allColors.Count - 1
            Dim thisCol = Color.FromName(allColors(i))
            If (c.R = thisCol.R) AndAlso (c.G = thisCol.G) AndAlso (c.B = thisCol.B) Then
                Return thisCol
            End If
        Next
        Return Color.FromArgb(c.R, c.G, c.B)
    End Function

End Class




I removed all of the extraneous designtime Properties from the Control, this time by using a ControlDesigner class, as this gives the Properties Window a much less cluttered appearance.
This ControlDesigner has just 1 Overridden sub-Procedure, where any Properties not named in the keep() array are removed.


Imports System.Windows.Forms.Design

Public Class borderControlDesigner
    Inherits ControlDesigner

    Protected Overrides Sub PreFilterProperties(ByVal properties As System.Collections.IDictionary)
        MyBase.PreFilterProperties(properties)

        'this removes all of the control's properties except those contained in the keep() array 
        Dim pi As List(Of DictionaryEntry) = properties.Cast(Of DictionaryEntry).ToList
        Dim keep() As String = {"AutoSize", "BackColor", "BorderColors", "Font", "ForeColor", "Location", "Name", "Size"}

        For x As Integer = pi.Count - 1 To 0 Step -1
            If Not keep.Any(Function(s) pi(x).Key.ToString = s OrElse pi(x).Key.ToString.StartsWith(s & "_")) Then
                properties.Remove(pi(x).Key.ToString)
            End If
        Next

    End Sub

End Class