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