wpfvb.netdesign-time

WPF: Design time support for dependency properties with default values


I have written a custom control based on a ListBox. It contains a default ItemTemplate which shows an image given to the ListBox by a custom dependency property. The control also contains a default image, which is used when the user doesn't give an image to the dependency property.

This works so far, but now I've found a little problem and I don't know how to fix that.

When I use my custom control in the XAML designer, it first shows the default image. When I set the image's dependency property to an other image, the new image is immediately shown in the XAML designer.

But when I remove the XAML attribute for the new image again, the XAML designer only shows a white rectangle instead of the default image.

I assume it's because with setting the image's dependency property to some value and then removing it I nulled the value. But even when I check for null in the CoerceCallback and give back the default image when the coerced value is null, doesn't work.

What's the best way to support fallback values for dependency properties?


TestControl.vb

Public Class TestControl
    Inherits ListBox

    Private Shared _defaultResources As ResourceDictionary

    Shared Sub New()
        _defaultResources = New ResourceDictionary
        _defaultResources.Source = New Uri("...")
    End Sub

    Public Shared ReadOnly TestProperty As DependencyProperty = DependencyProperty.Register(NameOf(TestControl.Test),
                                                                                            GetType(ImageSource),
                                                                                            GetType(TestControl),
                                                                                            New FrameworkPropertyMetadata(Nothing,
                                                                                                                          AddressOf TestControl.OnTestChanged,
                                                                                                                          AddressOf TestControl.OnTestCoerce))

    Public Property Test As ImageSource
        Get
            Return DirectCast(MyBase.GetValue(TestControl.TestProperty), ImageSource)
        End Get
        Set(value As ImageSource)
            MyBase.SetValue(TestControl.TestProperty, value)
        End Set
    End Property

    Private Shared Sub OnTestChanged(d As DependencyObject, e As DependencyPropertyChangedEventArgs)

    End Sub

    Private Shared Function OnTestCoerce(d As DependencyObject, value As Object) As Object
        If (value Is Nothing) Then
            Return TryCast(_defaultResources.Item("TestImage"), ImageSource)
        End If

        Return value
    End Function

    Public Sub New()
        Me.Test = TryCast(_defaultResources.Item("TestImage"), ImageSource)
    End Sub
End Class

When I use that control like this

<local:TestControl ItemsSource="{Binding Items}" />

every item shows the default image at design time. When I change the XAML to

<local:TestControl ItemsSource="{Binding Items}"
                   Test="{StaticResource NewImage}" />

every item shows the new item at design time. But when I remove the Test="{StaticResource NewImage}" again, it doesn't go back to the default image.


Solution

  • Ok, after some testing (using this technique) I have discovered the source of your issue.

    First of all, you are not using PropertyMetadata to set your default value, but instead the constructor. I assume you have a good reason to do so, but this essentially means that now you are relying on the coercion callback to set the default value.

    However, it is not called (the framework assumes that your "true" default value - Nothing - doesn't need to be validated) after you remove the
    Test="{StaticResource TestString}" line. Only the OnTestChanged is called. This means we can use it to restore the default value:

    void OnTestChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) 
    {
       if (e.NewValue is null)
       {
          ((TestControl)d).Test = yourDefaultImage;
          return;
       }
    
       //Actual "OnChanged" code
    }
    

    A clumsy solution indeed, but it works. Depending on your exact situation, you might also want to take a look at Binding's FallbackValue and TargetNullValue properties:
    Test="{Binding Source={ }, FallbackValue={ }, TargetNullValue={ }}"