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?
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>
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.
Related
Consider the following simple XAML:
<TextBlock x:Class="MyTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Foreground="Red">
</TextBlock>
And the associated C# code:
public partial class MyTextBlock : TextBlock
{
public MyTextBlock()
{
}
}
Why does the Foreground="Red" part not work when I then do <MyTextBlock Text="Foo"/> in my application? I know there are plenty of other ways to do it in code, but I have trouble understanding what is happening exactly here.
I have also tried the following XAML:
<TextBlock x:Class="MyTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Red"/>
</Style>
</TextBlock.Style>
</TextBlock>
And finally this one:
<TextBlock x:Class="MyTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<TextBlock.Resources>
<Style x:Key="MyTextBlockStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="Red"/>
</Style>
</TextBlock.Resources>
<TextBlock.Style>
<DynamicResource ResourceKey="MyTextBlockStyle"/>
</TextBlock.Style>
</TextBlock>
Again, neither of these appear to apply the style. Why is that? Is there a way to declare some kind of default style for the root element in XAML?
Why does the Foreground="Red" part not work when I then do <MyTextBlock Text="Foo"/> in my application?
It works if you call InitializeComponent():
public partial class MyTextBlock : TextBlock
{
public MyTextBlock()
{
InitializeComponent();
}
}
Local values take precedence over Style setters.
I'm working with WPF and I want to use attached properties to work with some styling things in the validation of the controls (my example of the problem is really simple, binding a simple text).
This is my attached property:
public class ToolTipExtension
{
public static readonly DependencyProperty ShowToolTipProperty = DependencyProperty.RegisterAttached(
"ShowToolTip", typeof(string), typeof(ToolTipExtension), new PropertyMetadata("Deffault"));
public static void SetShowToolTip(DependencyObject element, string value)
{
element.SetValue(ShowToolTipProperty, value);
}
public static string GetShowToolTip(DependencyObject element)
{
return (string) element.GetValue(ShowToolTipProperty);
}
}
I have a simple style dictionary like this
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:at="clr-namespace:CarpetaTecnicaWPF.AttachedProperties"
>
<Style TargetType="{x:Type TextBox}" x:Key="Blah" >
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Grid>
<TextBlock Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(at:ToolTipExtension.ShowToolTip)}" FontSize="50"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
As you can see, I'm trying to bind the Text property to my attached property.
In my Page, I'm using the style like this:
<TextBox Style="{StaticResource Blah}" at:ToolTipExtension.ShowToolTip="Prueba?"/>
The thing is, the value Prueba? does not appear. When I inspect the tree, I see this:
But in runtime, the result of the binding is Deffault
What am I doing wrong?
Your binding is incorrect.
The TemplatedParent in this case is not what you actually need. The ControlTemplate for the error is not applied to the text box itself, it's a stand-alone control template. So you are just getting a default value from a wrong FrameworkElement.
To access the text box your error template is applied to, you need to use the AdornedElementPlaceholder in your ControlTemplate. From that AdornedElementPlaceholder, you can access your text box via the AdornedElement property.
Here is an example:
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<Grid>
<AdornedElementPlaceholder x:Name="adorner"/>
<TextBlock Text="{Binding ElementName=adorner, Path=AdornedElement.(at:ToolTipExtension.ShowToolTip)}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
I am currently trying to conditionaly display an image. I read quite a bit about valueConverters and triggers, but i strongly believe that there has to be an easier solution for this easy problem.
The XAML:
<Image Source="C:\Users\Niko\Pictures\red.png" IsEnabled="{Binding IsOn}"></Image>
The code behind:
namespace MVVM {
public class Globals
{
int i = 2;
public bool IsOn
{
get
{
if (i == 1 )
return true;
else
return false;
}
}
}
I played around with the integer i to see if the image gets displayed or not. Any advice is greatly apreciated!
Bind the Image's Visibility to IsOn and use the built in BooleanToVisibilityConverter.
<Image Source="C:\Users\Niko\Pictures\red.png" Visibility="{Binding Visibility, Converter={StaticResource BoolToVis}}"/>
Then add the BooleanToVisibilityConverter as a static resource in either the <Window.Resources> for just that window or <Application.Resources> for your whole application.
<BooleanToVisibilityConverter x:Key="BoolToVis"/>
Note that x:Key is the name that you use to reference the converter after StaticResource.
If you don't want to put a Visibility property in your ViewModel and don't want to use converters, you can use a DataTrigger (here I don't have a ViewModel at all, the image is visible if the ToggleButton is checked):
<Image Source="C:\Users\Niko\Pictures\red.png">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Visibility"
Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=tg}"
Value="False">
<Setter Property="Visibility"
Value="Hidden" />
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
<ToggleButton Name="tg" Content="Show" HorizontalAlignment="Left" VerticalAlignment="Bottom" />
I've got a UserControl that contains a button:
<Button Content="Button"/>
And a style:
<Style TargetType="Button">
<Setter Property="Background" Value="Blue"/>
</Style>
The parent window (or another UserControl) may set another more general style:
<Style TargetType="Button">
<Setter Property="Background" Value="Red"/>
</Style>
The result is (what is obvious) that parent buttons will have more general style (Red) and my user control will have buttons with more specific style (Blue).
I'm wondering how to invert such behaviour in order to achieve something like setting the default style in my custom user control which could be then overriden in parent control or window if necessary?
The key is, that default style is defined first in custom user control and it is overriden automaticly by its parent. That is way I called it an inversion.
The imaginary example of the solution maight look like the following:
<Style TargetType="Button" StylePriority="Default">
<Setter Property="Background" Value="Blue"/>
</Style>
The StylePriority could indicate that if there is no other style defined for that button, then the default style should be applied to it.
You could use dynamic resources.
A UserControl:
<UserControl x:Class="Example.UserControl1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Example">
<UserControl.Resources>
<Style TargetType="local:UserControl1">
<Style.Resources>
<Style TargetType="Button" x:Key="UserControl1.DefaultButtonStyle">
<Setter Property="Background" Value="Red"/>
</Style>
</Style.Resources>
</Style>
</UserControl.Resources>
<Button Content="UserControlButton" Style="{DynamicResource UserControl1.DefaultButtonStyle}"/>
</UserControl>
And a Window:
<Window x:Class="Example.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:Example">
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Blue" />
</Style>
</Window.Resources>
<StackPanel>
<local:UserControl1 >
<local:UserControl1.Resources>
<Style x:Key="UserControl1.DefaultButtonStyle" TargetType="Button"
BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="FontSize" Value="40" />
</Style>
</local:UserControl1.Resources>
</local:UserControl1>
<Button Content="WindowButton" />
</StackPanel>
</Window>
If you remove the style for the control in the window, the default user control button style will be applied.
Create a dependency property in your UserControl for the buttons colour, and then bind to it. You can specify a default value of blue for that property.
public static readonly DependencyProperty ButtonColorProperty =
DependencyProperty.Register("ButtonColor", typeof(Color), typeof(MyUserControl),
new PropertyMetadata(Colors.Blue));
public Color State
{
get { return (Color)this.GetValue(ButtonColorProperty); }
set { this.SetValue(ButtonColorProperty, value); }
}
<UserControl ...
x:Name="root">
<Button Content="Button" Background="{Binding ElementName=root, Path=ButtonColor}" />
</UserControl>
Then set that property to red where you want to use the UserControl.
<local:MyUserControl ButtonColor="Red" />
Aim is to Reuse this MultiBinding in Various Places
<MultiTrigger.Conditions>
<Condition Value="True">
<Condition.Binding>
<MultiBinding Converter="{StaticResource ValidationBooleanConverter}">
<Binding X" />
<Binding Y" />
<Binding Z" />
</MultiBinding>
</Condition.Binding>
</Condition>
</MultiTrigger.Conditions>
Current not very DRY conditions (n+)
<Style x:Key="AnErrorTemplate" TargetType="FrameworkElement">
<Style.Triggers>
<MultiTrigger>
<!-- Repeating the whole MultiTrigger.Conditions block here --->
<Setter Property="Validation.ErrorTemplate" Value="{StaticResource DirtyErrorControlTemplate}" />
</MultiTrigger>
</Style.Triggers>
<Style x:Key="AnotherStyle" TargetType="FrameworkElement">
<Style.Triggers>
<MultiTrigger>
<!-- Repeating the whole MultiTrigger.Conditions block here --->
<Setter Property="Other" Value="SomeValueIfTheSameConditionsAreTrue" />
</MultiTrigger>
</Style.Triggers>
In fact, the requirements are broader, because I also need to re-use these same conditions in ControlTemplates.
<ControlTemplate x:Key="InlineErrorControlTemplate" TargetType="{x:Type Control}">
<TextBlock Text="{Binding ElementName=InputView, Path=(Validation.Errors)[0].ErrorContent}" Foreground="Red"/>
<ControlTemplate.Triggers>
<MultiTrigger>
<!-- Repeating the whole MultiTrigger.Conditions block here --->
<Setter Property="Visibility" Value="SetMeIfTheseSameTriggerConditionsHold" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
An umbrella for easy maintenance?
Any ideas how I could specify the MultiTrigger.Conditions or the MultiBinding once only, and then use it in multiple styles and control templates?
In XAML only?
This is exactly why Style inheritance was supplied in WPF. Plus in your case I will recommend to use DataTrigger with MultiBinding instead of MultiTrigger with MultiBinding ...
To demonstrate this lets assume I am creating a style that checks if the Tooltip or Name assigned to any FrameworkElement is empty. If so, it will make the Foreground red if the FrameworkElement is a ComboBox or its Background yellow if the FrameworkElement is TextBox.
I am using the miscellaneous field Tag of FrameworkElement....
So an all XAML (plus a C# converter) solution to this is as follows...
<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Height="300" Width="300">
<Window.Resources>
<local:AtleastOneEmptyConverter x:Key="AtleastOneEmptyConverter"/>
<Style x:Key="BaseFrameworkElementStyle"
TargetType="{x:Type FrameworkElement}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{StaticResource AtleastOneEmptyConverter}">
<Binding Path="ToolTip"
RelativeSource="{RelativeSource Self}"/>
<Binding Path="Name"
RelativeSource="{RelativeSource Self}"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Tag" Value="1"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="ApplyToComboStyle"
TargetType="{x:Type ComboBox}"
BasedOn="{StaticResource BaseFrameworkElementStyle}">
<Style.Triggers>
<DataTrigger
Binding="{Binding Tag,
RelativeSource={RelativeSource Self}}" Value="1">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="ApplyToTextBoxStyle"
TargetType="{x:Type TextBox}"
BasedOn="{StaticResource BaseFrameworkElementStyle}">
<Style.Triggers>
<DataTrigger
Binding="{Binding Tag,
RelativeSource={RelativeSource Self}}" Value="1">
<Setter Property="Background" Value="Yellow"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<StackPanel>
<ComboBox Style="{StaticResource ApplyToComboStyle}"
x:Name="NotRedComboBox"
ToolTip="You will not see red text here">
<ComboBoxItem IsSelected="True">I am not Red!</ComboBoxItem>
</ComboBox>
<ComboBox Style="{StaticResource ApplyToComboStyle}">
<ComboBoxItem IsSelected="True">I am Red!</ComboBoxItem>
</ComboBox>
<Separator Margin="5"/>
<TextBox Style="{StaticResource ApplyToTextBoxStyle}"
Text="I am yellow"
x:Name="YellowTextBox"/>
<TextBox Style="{StaticResource ApplyToTextBoxStyle}"
Text="I am not yellow"
x:Name="NotYellowTextBox"
ToolTip="You will not see yellow background here"/>
</StackPanel>
</Window>
C# Converter:
public class AtleastOneEmptyConverter : IMultiValueConverter
{
public object Convert
(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
return values.Cast<string>().Any(val => string.IsNullOrEmpty(val));
}
public object[] ConvertBack
(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I've been in this situation many times where I want to reuse commonly used Bindings that set Converters, Validation Rules within those Converters, Notifying on Source/Target update, etc. by inheriting from Binding and reusing them by specifying:
Foreground="{b:CustomColorBinding CustomDateTime}"
to reduce the amount of XAML that I need to type for bindings, but don't believe there's a way to keep it all in XAML. Trying to set the x:Key and use it as you would a Static or Dynamic Resource isn't possible, unfortunately. I've become so accustomed to using this convention that I've created a BindingCatalog for my projects to store my common (multi) binding scenarios for different controls that bind to different types.
I'm sure you have good reasons to want to avoid code-behind but if you can create your MultiBinding in code once and reuse it to DRY up your XAML -- then I think it more than justifies the (overly-demonized, IMHO) code that will be required to do so.
Hope that helps in deciding what to do!
An extended binding
namespace MyNamespace
{
public class CustomBinding : Binding
{
public CustomBinding(String path)
: base(path)
{
this.Converter = new CustomValueConverter();
this.UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
this.ValidatesOnDataErrors = true;
}
}
}
Its XAML usage
<UserControl x:Class="MyNamespace.UserControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyNamespace">
<StackPanel>
<TextBox Text="{local:CustomBinding MyViewModelProperty}"/>
</StackPanel>
</UserControl>