How to pass the binding path to DataTrigger in a WPF style? - wpf

I've several style with only a little difference in binding path:
<Style TargetType="FrameworkElement" x:Key="FieldValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=FieldValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="FrameworkElement" x:Key="NumberValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=NumberValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="FrameworkElement" x:Key="TextValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=TextValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="FrameworkElement" x:Key="DateTimeValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=DateTimeValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="FrameworkElement" x:Key="DateValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=DateValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="FrameworkElement" x:Key="TimeValidationErrorStyle">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=TimeValidationError}" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
We can find the only difference between the styles is the {Binding Path=xyz} in DataTrigger, can I remove the duplication with only XMAL markup? I know we can create custom styles as in this question but the setters are hard coded - can we only extend DataTrigger?

You can use a single style with a MultiBinding DataTrigger and an OR Converter in it.
<Window.Resources>
<local:AtleastOneEmptyConverter x:Key="AtleastOneEmptyConverter"/>
<Style TargetType="{x:Type FrameworkElement}">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding
Converter="{StaticResource AtleastOneEmptyConverter}">
<Binding Path="FieldValidationError"/>
<Binding Path="NumberValidationError"/>
<Binding Path="TextValidationError"/>
...
<Binding Path="DateTimeValidationError"/>
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
Code Behind:
public class AtleastOneEmptyConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType,
object parameter, CultureInfo culture)
{
return values.Cast<string>().Any(p => string.IsNullOrEmpty(p));
}
public object[] ConvertBack(object value, Type[] targetTypes,
object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
I do suggest, to skip applying styles to all framework elements in your application. It can make your application GUI slow if it grows complex in the future and seek an alternate approach such as Validation Model of WPF.
If you still want to apply style to all types of framework elemetns in your application then you can alternately explore App.xaml way to override styles of all framework elements in your application...

Seems like you want to create a template for a DataTrigger. Since you can't style DataTriggers, you have to resort to creating your own:
public class CollapsingDataTrigger : DataTrigger
{
public CollapsingDataTrigger()
{
base.Setters.Add(new Setter(FrameworkElement.VisibilityProperty, Visibility.Collapsed));
}
}
Then update your XAML to use this CollapsingDataTrigger instead of the regular one(you will have to reference the namespace this is created in).

Related

Make background of DataGrid cell change colour if value is negative/positive in XAML

Self explanatory question but for further clarity, the text in the cell of the PnL column will be random such as "-423.21" or "73.21". I want it to simply change the cell background green if it is above 0 and red if it is below.
This is what i tried:
<DataGridTextColumn Header="PnL" Binding="{Binding PnL}" Width="Auto">
<DataGridTextColumn.ElementStyle>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding PnL}" Value="0">
<Setter Property="Background" Value="Orange" />
</DataTrigger>
<DataTrigger Binding="{Binding PnL}" Value="{x:Static sys:Double.MinValue}">
<Setter Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding PnL}" Value="{x:Static sys:Double.MaxValue}">
<Setter Property="Background" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.ElementStyle>
</DataGridTextColumn>
You can use one converter for the brush property. That can encompass all logic you like for the brush colour.
Here's one I created earlier does just red and green.
This is generic and checks the text of the control but you could bind a specific known property instead. You could also pass the different brushes as properties of the markupextension.
public class MoneyValueToBrushConverter : MarkupExtension, IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double num;
if (!Double.TryParse(value.ToString(), out num))
{
return Brushes.Black;
}
return num > 0 ? Brushes.Green : Brushes.Red;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
}
You use that with the background brush:
<DataGridTextColumn.ElementStyle>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Background" Value="{Binding Text, RelativeSource={RelativeSource Self}, Converter={local:MoneyValueToBrushConverter}}"/>
</Style>
</DataGridTextColumn.ElementStyle>
Here it is working in a datagrid I have:
You could use two value converters for this, a less-than converter and a greater-than converter.
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding PnL}" Value="0">
<Setter Property="Background" Value="Orange" />
</DataTrigger>
<DataTrigger Binding="{Binding PnL,
Converter={StaticResource LessThanConverter},
ConverterParameter=0"
Value="True">
<Setter Property="Background" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding PnL,
Converter={StaticResource GreaterThanConverter},
ConverterParameter=0"
Value="True">
<Setter Property="Background" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
MSN has documentation on creating converters. https://learn.microsoft.com/en-us/dotnet/desktop/wpf/data/how-to-convert-bound-data
You could also just use one of the two converters with two triggers for True and False but you would need to make sure your DataTrigger for Value="0" is the last trigger in declared order or you will not get the Orange background when the value is 0.

Data trigger check value of dynamic resource

I have a WPF application and the content of should be displayed in German and English. Therefore, I created two seperate resource dictionaries which contain string snippets in each language. It is possible to switch between the languages while the application is running.
At this point I got stuck at a problem. There are some settings the user can make. If the setting was completed successfully a message shows up. The text of the message is taken from the resource dictionary. Based on a success or error message the text is displayed green or red.
<TextBlock Text="{Binding UpdateTaxPercentageSettingsMessage}" FontWeight="Bold">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{DynamicResource tax_percentage_update_went_wrong}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{StaticResource active_tax_law_update_went_wrong}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{StaticResource tax_percentage_was_updated_successfully}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{StaticResource active_tax_law_was_updated_successfully}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{StaticResource differential_taxation_info_update_went_wrong}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding UpdateTaxPercentageSettingsMessage}" Value="{StaticResource differential_taxation_info_was_updated_successfully}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
To make switching languages possible while the application is running, the string snippets taken from the resource dictionary has to be a DynamicResource. Unfortunately, I can not use DynamicResources as condition in data triggers. Has anyone faced a similar problem yet? I am grateful for your suggestions!
Do not use triggers on localized text, neither does it work with DynamicResouce, because Value is not a dependency property, nor is it readable. Instead, create an enum that describes your errors.
public enum ErrorType
{
WhoCares, // No comment on this.
ThisIsSuspicious, // Suspicous value.
ItsATrap, // Admiral Ackbar warned us.
ItIsNotWhatYouThinkItIs, // It is exactly what you think.
ItCannotBeThatSerious, // Serious Sam approves.
WhatDoesTheFlashingRedLightMean // If it is still flashing, how bad can it be, really?
}
Expose anothe property for the error and implement INotifyPropertyChanged if necessary.
public ErrorType ErrorType { get; }
Use the property instead of your resources as Value of the triggers.
<TextBlock Text="{Binding UpdateTaxPercentageSettingsMessage}" FontWeight="Bold">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.WhoCares}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.ThisIsSuspicious}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.ItsATrap}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.ItIsNotWhatYouThinkItIs}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.ItCannotBeThatSerious}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding ErrorType}" Value="{x:Static local:ErrorType.WhatDoesTheFlashingRedLightMean}">
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
Bonus round. As your data triggers are repetitive (they set the same values often), consider using a Binding with a custom converter that simply says, if the bound value matches any of the given values, apply this setter.
public class IsMatchingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(parameter is IEnumerable enumerable))
return false;
return enumerable.Cast<object>().Contains(value);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new InvalidOperationException();
}
}
Create an instance of the converter in any resource dictionary in scope.
<Window.Resources>
<local:IsMatchingConverter x:Key="IsMatchingConverter"/>
</Window.Resources>
Change the data triggers and pass the target values in an array as converter parameters.
<TextBlock Text="{Binding UpdateTaxPercentageSettingsMessage}" FontWeight="Bold">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<Binding Path="ErrorType" Converter="{StaticResource IsMatchingConverter}">
<Binding.ConverterParameter>
<x:Array Type="local:ErrorType">
<local:ErrorType>WhoCares</local:ErrorType>
<local:ErrorType>ThisIsSuspicious</local:ErrorType>
<local:ErrorType>ItCannotBeThatSerious</local:ErrorType>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Value="True">
<DataTrigger.Binding>
<Binding Path="ErrorType" Converter="{StaticResource IsMatchingConverter}">
<Binding.ConverterParameter>
<x:Array Type="local:ErrorType">
<local:ErrorType>ItsATrap</local:ErrorType>
<local:ErrorType>ItIsNotWhatYouThinkItIs</local:ErrorType>
<local:ErrorType>WhatDoesTheFlashingRedLightMean</local:ErrorType>
</x:Array>
</Binding.ConverterParameter>
</Binding>
</DataTrigger.Binding>
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>

WPF pass ElementName as parameter into a Style

I have 3 TextBlocks whose visibility property depends on a validation rule on 3 different elements (TextBoxes). How can I refactor this code so that the ElementName is abstracted away and I just have one style defined instead of 3? Is it possible to pass in ElementName as a parameter at the TextBlocks which this style is applied to?
<Style x:Key="textBlock_stockPriceWarning" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=stockPriceTextBox}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=stockPriceTextBox}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="textBlock_taxRateWarning" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=ordinaryTaxRateBox}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=ordinaryTaxRateBox}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="textBlock_capGainsTaxRateWarning" TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=capitalGainsTaxRateBox}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError), ElementName=capitalGainsTaxRateBox}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
**Update: Per Mark's advice below, I tried using the following ControlTemplate and attached property definition:
<ControlTemplate x:Key="LabelWarning" TargetType="Label">
<Border BorderBrush="AliceBlue"/>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding Path=(Validation.HasError),
ElementName={Binding Path=(views:ElementNameHelper.ElementAlias), RelativeSource={RelativeSource Mode=TemplatedParent}}}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=(Validation.HasError),
ElementName={Binding Path=(views:ElementNameHelper.ElementAlias), RelativeSource={RelativeSource Mode=TemplatedParent}}}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
public static class ElementNameHelper
{
public static readonly DependencyProperty ElementAliasProperty =
DependencyProperty.RegisterAttached("ElementAlias",
typeof(string), typeof(ElementNameHelper));
public static string GetElementAlias(DependencyObject obj)
{
return obj.GetValue(ElementAliasProperty).ToString();
}
public static void SetElementAlias(DependencyObject target, string value)
{
target.SetValue(ElementAliasProperty, value);
}
}
And invoking it like this:
<Label Content="Stock price should be a positive number."
FontSize="12" HorizontalAlignment="Center"
VerticalAlignment="Center" Foreground="Red"
Margin="1"
views:ElementNameHelper.ElementAlias="stockPriceTextBox"
Template="{StaticResource LabelWarning}"/>
But then the XAML compiler tells me that: A 'Binding' cannot be set on the 'ElementName' property of type 'Binding'. A 'Binding' can only be set on a DependencyProperty of a DependencyObject. Do I have this setup correctly and is it just the case that you simply can't use this approach on ElementName? Or am I missing something with the attached property definition?
Usual way to do this is with an attached property.
If you just want a cheap-n-easy solution then you can use Tag, which is described in the documentation as "an arbitrary object value that can be used to store custom information". An attached property is the better way to do it though.

wpf vb.net Datagrid image in row depending on value in cell

This is the code i use but it is notworking
<Window.Resources>
<Style x:Key="PinkRow" TargetType="{x:Type DataGridRow}">
<Style.Triggers>
<DataTrigger Binding="{Binding Rank}" Value="Master">
<Setter Property="Source" Value="A_Cancel.png"/>
</DataTrigger>
<DataTrigger Binding="{Binding RANK}" Value="Bosun">
<Setter Property="Background" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding RANK}" Value="">
<Setter Property="Background" Value="yellow" />
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
If field rank has the value "Master" i want the image A_cancel.png
The C# is case sensitive, so the Rank and RANK are different properties. Use proper property name in your bindings. Also you must mention in your Setter the TargetName for Image element.
<DataTrigger Binding="{Binding Rank}" Value="Master">
<Setter TargetName="ImageElementName" Property="Source" Value="A_Cancel.png"/>
</DataTrigger>

How to Use DataTrigger to set a property defined in the ViewModel in WPF

I am writing a XAML file which use DataTrigger to set a property in the ViewModel. The ViewModel class defined as:
public class ShellModel : INotifyPropertyChanged
{
public Brush ForegroundBrush
{
get; set;
}
....................
}
I want to use DataTrigger in the View.xaml to set the property ForegroundBrush. The XAML I wrote is:
<StatusBar Name="statusBar" Grid.Row="3">
<StatusBarItem>
<StatusBarItem.Style>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding HasError}" Value="True">
<Setter Property="ForegroundBrush" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding HasError}" Value="False">
<Setter Property="ForegroundBrush" Value="Black" />
</DataTrigger>
</Style.Triggers>
</Style>
</StatusBarItem.Style>
<TextBlock Name="statusBarMessage" Foreground="{Binding ForegroundBrush}" Text="{Binding StatusMessage}"></TextBlock>
</StatusBarItem>
........................
This does not compile. When I changed the
<Setter Property="ForegroundBrush" Value="Black" />
to
<Setter Property="ShellModel.ForegroundBrush" Value="Black" />
it gives me error:
Dependency property field missing ....
How shall I write this so that the DataTrigger can set the property ForegroundBrush in the ViewModel?
Setters in your DataTriggers should change properties of your UI elements only (and also they only work with DependencyProperties).
Set the Foregound Property of your StatusBarItem directly and set the TargetType of the Style. That should help.
<Style TargetType="{x:Type StatusBarItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding HasError}" Value="True">
<Setter Property="Foreground" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding HasError}" Value="False">
<Setter Property="Foreground" Value="Black" />
</DataTrigger>
</Style.Triggers>
</Style>
Having information about the visual representation in your ViewModel is usually not a good idea anyway.

Resources