I would like to have a dynamic ToolTip
style that changes the foreground color based on the text of different string properties (in this code example, the string is PosError
, but it could be any property name, and if the value is "Error", the DataTrigger
is triggered.
As for now, I have to do it like this for each and every TextBox
: Change the property binding of the DataTrigger
and the text of the ToolTip
(in this example, the string property is PosExtreme
):
<TextBox
Width="150"
FontSize="25"
MaxWidth="150"
Name="posTab"
TextAlignment="Center"
Text="{Binding PosTabStrength, UpdateSourceTrigger=PropertyChanged}">
<TextBox.ToolTip>
<ToolTip>
<ToolTip.Style>
<Style TargetType="{x:Type ToolTip}">
<Style.Triggers>
<DataTrigger Binding="{Binding PosError}" Value="Error">
<DataTrigger.Setters>
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#F08080" Offset="0"/>
<GradientStop Color="#F04080" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
<Setter Property="FontSize" Value="16"/>
<Setter Property="HasDropShadow" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#10F080" Offset="0"/>
<GradientStop Color="#10B080" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border
CornerRadius="3"
BorderThickness="1"
Background="#CF001233"
SnapsToDevicePixels="True"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<Border.Style>
<Style TargetType="{x:Type Border}">
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#A0A0F0" Offset="0"/>
<GradientStop Color="#8080F0" Offset="1"/>
<LinearGradientBrush.RelativeTransform>
<RotateTransform CenterX="0.5" CenterY="0.5"/>
</LinearGradientBrush.RelativeTransform>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
From="359"
To="0"
Duration="00:00:2"
Storyboard.TargetProperty="(Border.BorderBrush).(Brush.RelativeTransform).(RotateTransform.Angle)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<ContentPresenter Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ToolTip.Style>
<TextBlock Text="{Binding PosExtreme}"/>
</ToolTip>
</TextBox.ToolTip>
</TextBox>
I would like to have the ToolTip
style as ResourceDictionary
file and not having to copy and paste the code and modify it for every TextBox
with it's proper ToolTip
style and ToolTip
text.
Is this possible?
CodePudding user response:
Your main blocker for reusing the style are the bindings to concrete properties and the hard-coded Error
string. You need to pass them from outside of the style and control template.
Your comparison of the DataTrigger
can be encapsulated in a value converter that returns a boolean for the comparison of any bound property (PosError
) with a fixed value (Error
) passed as parameter.
public class ErrorComparisonToBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value != null && parameter != null && value.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
In the DataTrigger
, we do not bind a concrete property anymore, but the Tag
of the ToolTip
, which is a general purpose property.
Gets or sets an arbitrary object value that can be used to store custom information about this element.
We assume that the comparison is done for us already and the Tag
only contains the result true
or false
. This simplifies the DataTrigger
and the dependency on PosError
is gone.
Next, you can eliminate the TextBlock
inside the ToolTip
. ToolTip
is a ContentControl
and exposes a Content
property. This content is shown automatically using the ContentPresenter
inside the ControlTemplate
. The dependency on PosExtreme
is gone, too.
Move the style out in a resource dictionary in scope and create an instance of the converter, too.
<local:ErrorComparisonToBoolConverter x:Key="ErrorComparisonToBoolConverter"/>
<Style x:Key="MyToolTipStyle" TargetType="{x:Type ToolTip}">
<Style.Triggers>
<DataTrigger Binding="{Binding Tag, RelativeSource={RelativeSource Self}}" Value="True">
<DataTrigger.Setters>
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#F08080" Offset="0"/>
<GradientStop Color="#F04080" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
<Setter Property="FontSize" Value="16"/>
<Setter Property="HasDropShadow" Value="True" />
<Setter Property="OverridesDefaultStyle" Value="true" />
<Setter Property="Foreground">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#10F080" Offset="0"/>
<GradientStop Color="#10B080" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border
CornerRadius="3"
BorderThickness="1"
Background="#CF001233"
SnapsToDevicePixels="True"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<Border.Style>
<Style TargetType="{x:Type Border}">
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
<GradientStop Color="#A0A0F0" Offset="0"/>
<GradientStop Color="#8080F0" Offset="1"/>
<LinearGradientBrush.RelativeTransform>
<RotateTransform CenterX="0.5" CenterY="0.5"/>
</LinearGradientBrush.RelativeTransform>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Style.Triggers>
<EventTrigger RoutedEvent="Border.Loaded">
<BeginStoryboard>
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation
From="359"
To="0"
Duration="00:00:2"
Storyboard.TargetProperty="(Border.BorderBrush).(Brush.RelativeTransform).(RotateTransform.Angle)"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<ContentPresenter Margin="5" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You can reuse the style now and bind PosExtreme
or any other property to the Content
of ToolTip
and the error property to Tag
. The Tag
binding uses the value converter to compare the bound value to the expected error code passed as CommandParameter
.
<TextBox.ToolTip>
<ToolTip Style="{StaticResource MyToolTipStyle}"
Content="{Binding PosExtreme}"
Tag="{Binding PosError, ConverterParameter=Error, Converter={StaticResource ErrorComparisonToBoolConverter}}">
</ToolTip>
</TextBox.ToolTip>
Remarks
- The solution uses the
Tag
property to propagate the error property. In your example it works, but if you need to bind even more properties in the style or control template, you would have to create attached properties for each that you can bind instead ofTag
. For a single property,Tag
is just convenient. - You could also create a custom
ToolTip
control that exposes dependency properties for the properties used in the style and control template instead of attached properties and do the comparison within. - Depending on how your data items are implemented, you could eliminate the converter completely, by exposing a bool property, if that makes sense.