0

I've got a custom dependency property in my WPF UserControl called CustomForeground.

I'd like it to fallback to UserControl.ForeGround if no value is specified on CustomForeground.

I'm using the following code, which works, but it definitely feels a bit hacky.

Can anyone confirm confirm if there is a 'correct' way of implementing this dependency property?

public SolidColorBrush CustomForeground
{
      get { return (SolidColorBrush)(GetValue(CustomForegroundProperty) ?? GetValue(ForegroundProperty)); }
      set { SetValue(CustomForegroundProperty, value); }
}

Note - I've left out the declaration for the DependencyProperty as it's just boilerplate.

maxp
  • 24,209
  • 39
  • 123
  • 201
  • "if no value is specified" - does that mean if the property is never set, or should the fallback value also be used when the property is "reset" to e.g. null? Doing the in the getter of the property wrapper is definitely wrong, because the getter may be bypassed under certain circumstances. – Clemens Apr 11 '19 at 12:03
  • That won't work if you try and bind to `CustomForeground` -- bindings don't use the C# properties, they go directly to the DependencyProperty. It's the C# property that's just boilerplate - the DependencyProperty is where all the action happens! – canton7 Apr 11 '19 at 12:13
  • @canton7 CustomForeground is a dependency property of a UserControl. OP just omitted the declaration of the CustomForegroundProperty identifier field. – Clemens Apr 11 '19 at 12:20
  • No, it's a C# property which wraps a DependencyProperty. The actual DependencyProperty is the bit you said you left out, which starts `public static readonly DependencyProperty CustomForegroundProperty = ...`. The C# property is just to make it nicer to use the DependencyProperty from C# code - it is not used by bindings. – canton7 Apr 11 '19 at 12:22
  • One way to solve this is to have two properties - CustomForeground and CustomForegroundImpl. Create a MultiBinding from Foreground and CustomForegroundImpl to CustomForeground (TwoWay), and put a MultiValueConverter on the binding. When reading, the converter checks the value of CustomForegroundImpl, and if it's null returns the value of Foreground. When writing, it writes through to CustomForegroundImpl. Although I admit it's pretty nasty, it should work, and also notifies a change in CustomForeground when Foreground changes. – canton7 Apr 11 '19 at 12:24
  • 1
    @canton7 Perhaps read the question again. There is a dependency property (as usual with DependencyProperty identifier field and CLR wrapper). The question is how to make this dependency property return the value of the control's `Foreground` property when it hasn't been set. – Clemens Apr 11 '19 at 12:29
  • I read the question, see my comment just above. If there's a specific part of my comment you don't understand, or you think doesn't address your question, please highlight it specifically. – canton7 Apr 11 '19 at 12:29
  • @canton7 Do you even realize that I am not the one who asked the question? – Clemens Apr 11 '19 at 12:38
  • I missed that it's true, apologies. I'm still not sure what you're trying to tell me, though. – canton7 Apr 11 '19 at 12:38

3 Answers3

3

You might add a Style to your UserControl with a Setter for the CustomForeground property that sets a Binding to its Foreground property.

The Binding is used unless the CustomForeground property value is replaced by another Binding or local value, or animation etc.

<UserControl ...>
    <UserControl.Style>
        <Style>
            <Setter
                Property="local:MyUserControl.CustomForeground"
                Value="{Binding Foreground, RelativeSource={RelativeSource Self}}"/>
        </Style>
    </UserControl.Style>
    ...
</UserControl>
Clemens
  • 123,504
  • 12
  • 155
  • 268
  • This is very nice - much cleaner than mine! You just have to make sure that nothing else sets the `Style` on that control. – canton7 Apr 11 '19 at 13:10
0

This should do the trick:

public partial class MyControl : UserControl
{
    public MyControl()
    {
        InitializeComponent();

        var multiBinding = new MultiBinding()
        {
            Converter = FallbackColorConverter.Instance,
            Mode = BindingMode.TwoWay,
            Bindings =
            {
                new Binding()
                {
                    Source = this,
                    Path = new PropertyPath(CustomForegroundBackingProperty),
                    Mode = BindingMode.TwoWay
                },
                new Binding()
                {
                    Source = this,
                    Path = new PropertyPath(ForegroundProperty),
                    Mode = BindingMode.OneWay
                },
            },
        };

        SetBinding(CustomForegroundProperty, multiBinding);
    }

    public Brush CustomForeground
    {
        get => (Brush)GetValue(CustomForegroundProperty);
        set => SetValue(CustomForegroundProperty, value);
    }
    public static readonly DependencyProperty CustomForegroundProperty =
        DependencyProperty.Register(nameof(CustomForeground), typeof(Brush), typeof(MyControl), new PropertyMetadata(null));

    private static readonly DependencyProperty CustomForegroundBackingProperty =
        DependencyProperty.Register("CustomForegroundBacking", typeof(Brush), typeof(MyControl), new PropertyMetadata(null));

    private class FallbackColorConverter : IMultiValueConverter
    {
        public static readonly FallbackColorConverter Instance = new FallbackColorConverter();

        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
        {
            return values[0] ?? values[1];
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
        {
            return new object[] { value };
        }
    }
}

We set up two DependencyProperties. CustomForegroundBackingProperty acts as the actual store for any CustomForeground values the user sets. CustomForegroundProperty just acts as a sort of proxy.

When then set up a MultiBinding from ForegroundProperty and CustomForegroundBackingProperty to CustomForegroundProperty. The MultiBinding is set to TwoWay (so change any change to CustomForegroundProperty triggers the binding, as does any change to ForegroundProperty or CustomForegroundBackingProperty).

We set the binding to CustomForegroundBackingProperty to TwoWay (as we want writes to CustomForegroundProperty to affect CustomForegroundBackingProperty), but we set the binding to ForegroundProperty to OneWay, as we don't want this to happen.

Then we put a converter on the MultiBinding. When the converter writes to CustomForegroundProperty, it looks at both CustomForegroundBackingProperty and ForegroundProperty, and chooses ForegroundProperty if CustomForegroundBackingProperty is null. This is triggered if either ForegroundProperty or CustomForegroundBackingProperty changes.

When the converter writes the other way -- i.e. the user writes to CustomForegroundProperty -- then we just return the value they set. Because of the modes on the bindings in the MultiBinding, this means this value gets set to CustomForegroundBackingProperty.

canton7
  • 37,633
  • 3
  • 64
  • 77
0

I've had to go for a 'bottom-up' solution, rather than a 'top-down' like @Clemens (as I already had a style defined on UserControl. It looks something like this:

<Style TargetType="{x:Type TextBlock}">
    <Setter Property="Foreground" Value="{Binding ValueBrush}" />
    <Style.Triggers>
        <DataTrigger Binding="{Binding ValueBrush}" Value="{x:Null}">
            <Setter Property="Foreground" Value="{Binding Foreground, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type UserControl}}}" />
        </DataTrigger>
    </Style.Triggers>
</Style>
maxp
  • 24,209
  • 39
  • 123
  • 201
  • While this may somehow be a solution for you, it does not answer your question how to "fallback to UserControl.Foreground if no value is specified on CustomForeground". – Clemens Apr 11 '19 at 14:40