Search code examples
.netvb.netuser-controlstypeconverter

Changing custom child property doesn't update parent class's control


I don't know what I'm doing wrong, but I've created a custom control with a property that features subproperties using a type converter that inherits ExpandableObjectConverter.

It seems like I've got everything set up correctly, but when I try to change any of the subproperties of the parent property, the display in the designer doesn't change until I click another property (e.g. if I change the Color1 property of an object that uses my custom Gradient class for its color, the color will not change in the designer until I click off of that property or object).

The code is included below.


The IndicatorBar control class:

Imports System.ComponentModel

Public Class IndicatorBar
    Private _Percentage As Double
    Private ReadOnly _BackGradient, _BarGradient As Gradient
    Private _Side As SourceSide

    ...

    <Description("Expand to set the colors of the background gradient."), Category("Appearance"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    Public ReadOnly Property BackGradient As Gradient
        Get
            Return _BackGradient
        End Get
    End Property

    <Description("Expand to set the colors of the bar gradient."), Category("Appearance"), DesignerSerializationVisibility(DesignerSerializationVisibility.Content)>
    Public ReadOnly Property BarGradient As Gradient
        Get
            Return _BarGradient
        End Get
    End Property

    ...

    Private Sub IndicatorBar_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
        Dim backRect As New Rectangle(0, 0, Width, Height)
        Dim maskRect As RectangleF
        Dim bar As New Rectangle(0, 0, Width, Height)

        Select Case Side
            Case SourceSide.Left
                maskRect = New RectangleF(0, 0, Width * Percentage, Height)
            Case SourceSide.Top
                maskRect = New RectangleF(0, 0, Width, Height * Percentage)
            Case SourceSide.Right
                maskRect = New RectangleF(Width * (1.0 - Percentage), 0, Width * Percentage, Height)
            Case SourceSide.Bottom
                maskRect = New RectangleF(0, Height * (1.0 - Percentage), Width, Height * Percentage)
        End Select
        Using backGrad As New Drawing2D.LinearGradientBrush(backRect, BackGradient.Color1, BackGradient.Color2, BackGradient.Angle)
        e.Graphics.FillRectangle(backGrad, backRect)
        End Using
        e.Graphics.SetClip(maskRect)
        Using barGrad As New Drawing2D.LinearGradientBrush(bar, BarGradient.Color1, BarGradient.Color2, BarGradient.Angle)
            e.Graphics.FillRectangle(barGrad, bar)
        End Using
        e.Graphics.ResetClip()
    End Sub
End Class

The Gradient class:

Imports System.ComponentModel

<TypeConverter(GetType(GradientConverter))>
Public Class Gradient

    Private _Angle As UShort = 0
    Private _Color1, _Color2 As Color

    Public Sub New()
        Color1 = SystemColors.ControlLight
        Color2 = SystemColors.ControlLightLight
    End Sub

    Public Sub New(ByVal c1 As Color, ByVal c2 As Color)
        Color1 = c1
        Color2 = c2
    End Sub

    Public Sub New(ByVal c1 As Color, ByVal c2 As Color, ByVal ang As UShort)
        Color1 = c1
        Color2 = c2
        Angle = ang
    End Sub

    <Browsable(True), NotifyParentProperty(True), RefreshProperties(RefreshProperties.Repaint), EditorBrowsable(EditorBrowsableState.Always)>
    Public Property Color1 As Color
        Get
            Return _Color1
        End Get
        Set(value As Color)
            _Color1 = value
        End Set
    End Property

    <Browsable(True), NotifyParentProperty(True), RefreshProperties(RefreshProperties.Repaint), EditorBrowsable(EditorBrowsableState.Always)>
    Public Property Color2 As Color
        Get
            Return _Color2
        End Get
        Set(value As Color)
            _Color2 = value
        End Set
    End Property

    <Browsable(True), NotifyParentProperty(True), EditorBrowsable(EditorBrowsableState.Always), DefaultValue(0)>
    Public Property Angle As UShort
        Get
            Return _Angle
        End Get
        Set(value As UShort)
            _Angle = value Mod 360
        End Set
    End Property
End Class

The GradientConverter class:

Public Class GradientConverter
    Inherits ExpandableObjectConverter

    Public Overrides Function ConvertTo(context As ITypeDescriptorContext, culture As Globalization.CultureInfo, value As Object, destinationType As Type) As Object
        If destinationType Is GetType(String) Then
            Return ""
        End If
        Return MyBase.ConvertTo(context, culture, value, destinationType)
    End Function
End Class

Solution

  • First, turn on Option Strict there are 8 or so implicit conversions in your code. I'd also change the Angle type. LinearGradientBrush expects a single, but you have it as UShort.

    Part 1

    Since the Gradient property is itself a Type (class), you need to notify the parent when a child element changes. In other words in IndicartorBar.BarGradient.ColorX - you want a notification to bubble up 2 levels to IndicatorBar who performs the painting.

    I suspect that it what you were trying to do with NotifyParentProperty and RefreshProperties. The problem is that a change to Color1 will only notify Gradient. Additionally, attributes don't automatically interact with the Class or Property they decorate. Most often, they are instruction to something else like a Designer or Serializer.

    The solution is to implement INotifyPropertyChanged which is simple and doesn't change the code much:

    Public Class Gradient
        Implements INotifyPropertyChanged
    
        ...
        ' VS will add this when you press ENTER on the Implements line
        Public Event PropertyChanged(sender As Object,
             e As PropertyChangedEventArgs) Implements INotifyPropertyChanged.PropertyChanged
    
        ...
    
        Public Property Color2 As Color
            Get
                Return _Color2
            End Get
            Set(value As Color)
                If value <> _Color2 Then
                    _Color2 = value
    
                    RaiseEvent PropertyChanged(Me, 
                       New PropertyChangedEventArgs("Gradient"))
                End If
            End Set
        End Property
    

    Modify both Color1 and Color2 setters to fire the event. Then on IndicatorBar we need to subscribe to the event and respond:

    Public Class IndicatorBar
        ...
        ' I have no idea why these were ReadOnly
        Private WithEvents _BackGradient, _BarGradient As Gradient
        ...
    
        Private Sub _BackGradient_PropertyChanged(sender As Object,
            e As PropertyChangedEventArgs) Handles BackGradient.PropertyChanged, 
                   _BarGradient.PropertyChanged
    
            Me.Invalidate()
    
        End Sub
    

    Now, when either Color# changes on either Gradient as soon as the color picker dropdown closes, the UserControl will be notified and the control should be redrawn immediately as a result of Invalidate().

    To add it for Angle just raise the event in that setter as above. Also be sure to Clean and Rebuild the project before testing the changes.

    Part 2

    Another problem is that your TypeConverter isn't doing anything. It inherits from ExpandableObjectConverter, so the child properties collapse. But when collapsed, the TypeConverter is supposed to provide a summary as with Font or Location.

    Your ConvertTo is supposed to provide that summary if the child properties.

    Public Class GradientConverter
        Inherits ExpandableObjectConverter
    
        ' when the designer asks if we can convert to string,
        ' reply "Yes, I can!"
        Public Overrides Function CanConvertTo(context As ITypeDescriptorContext, 
                               destinationType As Type) As Boolean
    
            If destinationType Is GetType(String) Then
                Return True
            End If
    
            Return MyBase.CanConvertTo(context, destinationType)
        End Function
    
        Public Overrides Function ConvertTo(context As ITypeDescriptorContext,
                                            culture As Globalization.CultureInfo,
                                            value As Object,
                                            destinationType As Type) As Object
            If destinationType Is GetType(String) Then
                ' cast value to our Type
                Dim grad As Gradient = CType(value, Gradient)
    
                ' return the prop summary
                Return String.Format("{0}, {1}, {2}", grad.Angle.ToString,
                                        grad.Color1.ToString,
                                        grad.Color2.ToString)
            End If
            Return MyBase.ConvertTo(context, culture, value, destinationType)
        End Function
    End Class
    

    Now, your 2 Gradient properties will summarize the 3 child props, and update as you change them:

    enter image description here

    Note:
    If you wanted to allow the string form of the 3 child properties to be edited by the user (e.g. let them type "Blue" over one of the colors without expanding or opening the Gradient property, you'd have to implement CanConvertFrom / ConvertFrom to convert the text to valid property values.

    In such cases, be careful how you decorate or separate the individual values (e.g. the uses 2 commas in the text). Your ConvertFrom code will have to parse the formatted string to pull out those values (also keep in mind the user may have removed or changed the delimiters).