Daniel Hilgarth
Daniel Hilgarth

Reputation: 174397

Prevent IValueConverter from setting a local value

Short version:
WPF seems to always set a local value when using an IValueConverter in a binding, even if that converter returns Binding.DoNothing.
My question is: What do I have to return or do to tell WPF to use the inherited value?

Please note: I don't want to use DataTriggers as this would bloat up my code significantly because I would need one data trigger along with a converter for every color my current converter returns.


Long version with reproduction:

Imagine the following scenario:
I have a Button in which a TextBlock is located. There exists a style for the Button that sets the Foreground property. This value is inherited by the TextBlock. Now I want to create a value converter that converts the value of the TextBlock to a Brush to be used as the Foreground - but only in some cases. In the cases in which I don't want to set a special color, I return Binding.DoNothing. My understanding was that this would make the TextBlock to continue to use the inherited value.

Unfortunatelly, my understanding was not correct. Even when returning Binding.DoNothing a local value is set. This has been verified with Snoop.

The problem can be easily reproduced with this simple example:

XAML:

<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication1="clr-namespace:WpfApplication1"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <WpfApplication1:DummyConverter x:Key="DummyConverter" />
    <Style TargetType="{x:Type Button}">
      <Setter Property="Foreground" Value="Red" />
    </Style>
    <Style TargetType="{x:Type TextBlock}">
          <Setter Property="Foreground"
                  Value="{Binding Path=Text, RelativeSource={RelativeSource Self}, Converter={StaticResource DummyConverter}}" />
    </Style>
  </Window.Resources>
  <StackPanel>
    <Button><TextBlock>Text1</TextBlock></Button>
    <Button><TextBlock>Text2</TextBlock></Button>
  </StackPanel>
</Window>

Converter:

public class DummyConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value.ToString() == "Text2")
            return Brushes.Cyan;
        return Binding.DoNothing;
    }
}

As you can see, the first button has a black text instead of red. If you remove the style for TextBlock both buttons will have the correct red text.

Question:
What do I have to do to prevent this? Is there some value to return that tells the engine to continue using the inherited value?

Upvotes: 2

Views: 534

Answers (2)

Pakman
Pakman

Reputation: 2210

To answer your question: according to this thread, no. As soon as you give the TextBlock a style setter (#4), any value returned will override inherited properties (#7).

Instead, you could create a MultiBinding like so:

public class DummyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (values[0].ToString() == "Text2")
            return Brushes.Cyan;

        return values[1];
    }
}

<Window x:Class="Spritefire.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Spritefire"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <local:DummyConverter x:Key="DummyConverter" />
        <Style TargetType="{x:Type Button}">
            <Setter Property="Foreground" Value="Red" />
        </Style>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Foreground">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource DummyConverter}">
                        <Binding Path="Text" RelativeSource="{RelativeSource Self}" />
                        <Binding Path="Foreground" ElementName="ExampleButton" />
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </Window.Resources>
    <StackPanel>
        <Button x:Name="ExampleButton">
            <TextBlock>Text1</TextBlock>
        </Button>
        <Button>
            <TextBlock>Text2</TextBlock>
        </Button>
    </StackPanel>
</Window>

Upvotes: 3

Rafal
Rafal

Reputation: 12629

I've got it working but this solution isn't nice and smells a little... Nevertheless I'll post it.

Declare custom attached property like this:

 public static class CustomForeground
{
    public static readonly DependencyProperty CustomForegroundProperty = DependencyProperty.RegisterAttached(
                      "CustomForeground",
                      typeof(Brush),
                      typeof(CustomForeground),
                      new FrameworkPropertyMetadata(Brushes.Black, FrameworkPropertyMetadataOptions.AffectsRender, OnChange));

    public static void SetCustomForeground(UIElement element, Brush value)
    {
        element.SetValue(CustomForegroundProperty, value);
    }

    public static void OnChange(DependencyObject d, DependencyPropertyChangedEventArgs arg)
    {
        if (arg.NewValue != null)
            d.SetValue(TextBlock.ForegroundProperty, arg.NewValue);
    }
}

and text box style:

<Style TargetType="{x:Type TextBlock}">
        <Setter Property="WpfApplication1:CustomForeground.CustomForeground"
              Value="{Binding Path=Text, RelativeSource={RelativeSource Self}, Converter={StaticResource DummyConverter}}" />
    </Style>

This is the only thing that comes to my mind at the moment and it will work assuming that text of you buttons is not changing. If it is you would need to remember default value of Foreground in another attached property and set it in else statement.

Upvotes: 1

Related Questions