Hide DataTrigger if RelativeSource doesn't exist - wpf

I want to add a DataTrigger to my base TextBox style so that it sets the foreground color to a different value if it is inside of a DataGridCell that is selected. Here is what my trigger looks like:
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground"
Value="White" />
</DataTrigger>
</Style.Triggers>
This works great, except that when my TextBox is not in a DataGrid the Binding fails and writes an exception to the output window. How can I prevent this.
I basically want to say if Parent is a DataGridCell then apply this trigger otherwise ignore it.

In general just only apply the style where applicable. If you want implicit application use nested styles:
<Style TargetType="{x:Type DataGrid}">
<Style.Resources>
<Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
</Style.Resources>
</Style>
If you have other parts which you want to apply to all TextBoxes take out those parts in a serarate style and use BasedOn in the style which applies to the TextBoxes inside the DataGrid.
Edit: MultiDataTrigger seems to return right away if a condition is not met so you can avoid binding errors:
<Style TargetType="{x:Type TextBox}">
<Style.Resources>
<vc:HasAncestorOfTypeConverter x:Key="HasAncestorOfTypeConverter" AncestorType="{x:Type DataGridCell}" />
</Style.Resources>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition
Binding="{Binding RelativeSource={RelativeSource Self}, Converter={StaticResource HasAncestorOfTypeConverter}}"
Value="True" />
<Condition
Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}, Path=IsSelected}"
Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Foreground" Value="Red" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
public class HasAncestorOfTypeConverter : IValueConverter
{
public Type AncestorType { get; set; }
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value == null) return false;
DependencyObject current = value as DependencyObject;
while (true)
{
current = VisualTreeHelper.GetParent(current);
if (current == null)
{
return false;
}
if (current.GetType() == AncestorType)
{
return true;
}
}
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
This of course causes quite som overhead so it might not be such a good solution, then again if the RelativeSource-binding fails it also had to go up the tree first.

Related

WPF : How should I change the style of a button when his context id changed?

I have two button styles as static resources. BtnStyleOpen and BtnStyleClose.
A button with Name="V001" and in .cs I Bound the context with an object
BtnV001.Content = content; In content object there is a status property.
I would like to change the style of the button when the status is changed.
My code is:
<Button x:Name="Btn001" Grid.Column="5" Grid.Row="7"
Click="BtnV_Click" MouseRightButtonUp="BtnV_MouseRightButtonUp"
Content="{Binding Path=Status, UpdateSourceTrigger=PropertyChanged}">
<Button.Triggers>
<Trigger Property="Content" Value=1>
<Setter Property="Style" Value="{StaticResource BtnStyleOpen}" />
</Trigger>
<Trigger Property="Content" Value=0>
<Setter Property="Style" Value="{StaticResource BtnStyleClose}" />
</Trigger>
</Button.Triggers>
</Button>
try bind it to the event that invoke the change. try use datatrigger.
something like this :
<ToggleButton x:Name="togglebutton_Testing">
<TextBlock x:Name="Textblock_Testing">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsChecked, ElementName=togglebutton_Testing}" Value="false">
<Setter Property="Text" Value="Open"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsChecked, ElementName=togglebutton_Testing}" Value="true">
<Setter Property="Text" Value="Close"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</ToggleButton>
You can implement a IValueConverter and apply it to the binding between Button.Style and DataContext.Status:
public class StatusToStyleConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Status status)
{
if (status == "open")
{
return (Style) Application.Current.FindResource("BtnStyleOpen");
}
if (status == "close")
{
return (Style) Application.Current.FindResource("BtnStyleClose");
}
}
return Binding.DoNothing;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
You the have to add an instance of the IValueConverter?to some resouce:
<Button.Resource>
<StatusToStyleConverter x:Key"StatusToStyleConverter" />
<Button.Resource>
Then apply the Style:
<Button Style="{Binding Status, Converter={StaticResource StatusToStyleConverter}} />
As you didn't share details of your Status property type, you probably have to modify the condition evaluation in the converter.

WPF one trigger for multiple values

I'm trying to create a single trigger will trigger on multiple possible options. I want to set the background to green when options are either "Reviewed" or "Completed". Then I want a second trigger to change the background to Yellow when "Pending" or "Yellow".
This answer pointed me towards it, but was incomplete and i couldn't make sense of it: https://stackoverflow.com/a/4660030/526704
Here's what I have now:
<Style x:Key="StatusCellTextBlock" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="Reviewed">
<Setter Property="Background" Value="Green"/>
</Trigger>
<Trigger Property="Text" Value="Completed">
<Setter Property="Background" Value="Green"/>
</Trigger>
<Trigger Property="Text" Value="Pending">
<Setter Property="Background" Value="Yellow"/>
</Trigger>
<Trigger Property="Text" Value="Pending Review">
<Setter Property="Background" Value="Yellow"/>
</Trigger>
</Style.Triggers>
</Style>
Here's the kind of thing i'm looking for: (some way to specify multiple values of the property that trigger the same setters. I have many more of these that I'd like to condense without repeating the same trigger many times)
<Style x:Key="StatusCellTextBlock" TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="Reviewed" Value2="Completed">
<Setter Property="Background" Value="Green"/>
</Trigger>
<Trigger Property="Text" Value="Pending" Value2="Pending Review">
<Setter Property="Background" Value="Yellow"/>
</Trigger>
</Style.Triggers>
</Style>
You can create your custom IValueConverter that would convert string into SolidColorBrush
public class TextToBackgroundConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
switch ((string)value)
{
case "Reviewed":
case "Completed":
return new SolidColorBrush(Colors.Green);
case "Pending":
case "Pending Review":
return new SolidColorBrush(Colors.Yellow);
}
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
and then you don't even need Trigger. You can use Binding with Converter
<Style x:Key="StatusCellTextBlock" TargetType="TextBlock">
<Setter
Property="Background"
Value="{Binding RelativeSource={RelativeSource Self}, Path=Text, Converter={StaticResource TextToBackgroundConverter}}"/>
</Style>
where TextToBackgroundConverter is defined somewhere in resources as
<Window.Resources>
<local:TextToBackgroundConverter x:Key="TextToBackgroundConverter"/>
</Window.Resources>
I devised a solution. I'm now using a converter to determine whether or not the value is given among the provided parameters. I send the options "Reviewed,Completed" as a parameter to the converter, which returns true if the text from the TextBlock is found anywhere in the parameter.
XAML:
<Style TargetType="TextBlock" x:Key="StatusStyle">
<Style.Triggers>
<DataTrigger Value="True" Binding="{Binding Text, RelativeSource={RelativeSource Self}, Converter={StaticResource OrConverter}, ConverterParameter=Reviewed;Completed}">
<Setter Property="Foreground" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
Converter:
[ValueConversion(typeof(string), typeof(bool))]
public class MultiValueOrConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((string)parameter).Split(';').Contains((string)value);
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}

How to check if a row has odd number?

I'm trying to set different color for odd rows using XAML.
The datagrid in question has 3 different types of data, which I want to color differently, and simply changing AlternatingRowBackground won't do.
I'm planning on using something like
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding IsSelected, RelativeSource={RelativeSource Self}}" Value="False"/>
<Condition Binding="{Binding IsOddRow, RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
There doesn't seem to be such a property as IsOddRow. What property should I check instead?
You can set AlternationCount on the DataGrid and then bind to the ancestor DataGridRows attached property ItemsControl.AlternationIndex. If the value is "1" you have an odd row number.
<DataGrid ItemsSource="{Binding ...}"
AlternationCount="2">
<DataGrid.CellStyle>
<Style TargetType="DataGridCell">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding RelativeSource={RelativeSource Self},
Path=IsSelected}"
Value="False"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type DataGridRow}},
Path=(ItemsControl.AlternationIndex)}"
Value="1"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
<!-- ... -->
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<!-- ... -->
</DataGrid>
Note that when binding to an attached property, you must put parentheses around the attached property. Path=(ItemsControl.AlternationIndex) will work but Path=ItemsControl.AlternationIndex won't.
Update
You could also create the property IsOddRow through an attached behavior. In the behavior you subscribe to LoadingRow. In the event handler you get the index for the loaded row and check if it is odd or not. The result is then stored in an attached property called IsOddRow which you can bind to.
To start the behavior add behaviors:DataGridBehavior.ObserveOddRow="True" to the DataGrid
<DataGrid ItemsSource="{Binding ...}"
behaviors:DataGridBehavior.ObserveOddRow="True">
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding Type}" Value="0"/>
<Condition Binding="{Binding Path=IsSelected,
RelativeSource={RelativeSource Self}}" Value="False"/>
<Condition Binding="{Binding Path=(behaviors:DataGridBehavior.IsOddRow),
RelativeSource={RelativeSource Self}}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter Property="Background" Value="#FFDFE6ED"/>
</MultiDataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
DataGridBehavior
public class DataGridBehavior
{
#region ObserveOddRow
public static readonly DependencyProperty ObserveOddRowProperty =
DependencyProperty.RegisterAttached("ObserveOddRow",
typeof(bool),
typeof(DataGridBehavior),
new UIPropertyMetadata(false, OnObserveOddRowChanged));
[AttachedPropertyBrowsableForType(typeof(DataGrid))]
public static bool GetObserveOddRow(DataGrid dataGrid)
{
return (bool)dataGrid.GetValue(ObserveOddRowProperty);
}
public static void SetObserveOddRow(DataGrid dataGrid, bool value)
{
dataGrid.SetValue(ObserveOddRowProperty, value);
}
private static void OnObserveOddRowChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
DataGrid dataGrid = target as DataGrid;
dataGrid.LoadingRow += (object sender, DataGridRowEventArgs e2) =>
{
DataGridRow dataGridRow = e2.Row;
bool isOddRow = dataGridRow.GetIndex() % 2 != 0;
SetIsOddRow(dataGridRow, isOddRow);
};
}
#endregion // ObserveOddRow
#region IsOddRow
public static DependencyProperty IsOddRowProperty =
DependencyProperty.RegisterAttached("IsOddRow",
typeof(bool),
typeof(DataGridBehavior),
new PropertyMetadata(false));
[AttachedPropertyBrowsableForType(typeof(DataGridRow))]
public static bool GetIsOddRow(DataGridRow dataGridCell)
{
return (bool)dataGridCell.GetValue(IsOddRowProperty);
}
public static void SetIsOddRow(DataGridRow dataGridCell, bool value)
{
dataGridCell.SetValue(IsOddRowProperty, value);
}
#endregion // IsOddRow
}
I am not sure which grid/row type you are using, so I can't give you the exact property names, however, bind to the row's index (row number) and use a value converter (that returns true) to check if the row is odd or even.
Almost every answer use AlternationCount="2" but I find it a little bit too limiting. On my side I use something like AlternationCount="{ Binding MainData.ProjColl.Count}" in order to number my rows until the end ( take care it starts at 0 ! ).
In this case I need a value Converter as mentioned by #Danny Varod.
I use the converter to alternate the color of the rows ( nearly answering the question )
public class IsEvenConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
bool res = false;
int? val = value as int?;
if (null != val)
res = (0 == (val % 2));
return res;
}
...
}
And the calling XAML
<UserControl ...
<UserControl.Resources>
...
<vm_nmspc:IsEvenConverter x:Key="IsEven"/>
<Style TargetType="DataGridRow">
<Setter Property="Width" Value="Auto"/>
<Setter Property="Background" Value="LightGray"/>
<!--Converter will be used below-->
<Style.Triggers>
...
<Setter Property="Background" Value="LightGray"/
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self},
Path=(ItemsControl.AlternationIndex),
Converter={StaticResource ResourceKey=IsEven}}" Value="true">
<Setter Property="Background" Value="Lavender"/>
</DataTrigger>
<Trigger Property="IsMouseOver" Value="True" >
<Setter Property="Background" Value="LightGreen"/>
</Trigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Grid>
<DataGrid ItemsSource="{Binding MainData.ProjColl}" AutoGenerateColumns="False"
AlternationCount="{ Binding MainData.ProjColl.Count}" >
...
<DataGridTextColumn Header="Project Name" ....
</DataGrid>
</Grid>
</UserControl>
and some discrete screenshots
Another part of the same code:
Simple way to display row numbers on WPF DataGrid

WPF - XAML - Data Trigger without Binding / Value only trigger

I have a grid of values in a DataGrid. In each cell I have one of 4 values, I want each value to have an associated colour.
In the past all my data triggers have been of the format;
<DataTrigger Binding="{Binding Status}" Value="R">
<Setter Property="xcdg:DataRow.Background" Value="Pink" />
</DataTrigger>
But I need something of the format;
<DataTrigger Binding="{Binding *}" Value="R">
<Setter Property="xcdg:DataRow.Background" Value="Pink" />
</DataTrigger>
So that any cell's value will be checked. The reason I can't use explicit bindings is that the number of columns and their names is dynamic, I use extend CustomTypeDescriptor to expose the columns and rows to enter the grid.
Thanks in advance!
Is it an option to use a ValueConverter in your binding? That way you could check in the converter which value it should return.
<DataTrigger Binding="{Binding Path=Content, RelativeSource={x:Static RelativeSource.Self}}" Value="Failure">
<Setter Property="xcdg:DataRow.Background" Value="Red" />
</DataTrigger>
I needed to use a Relative source.
Further to that what I ended up using was;
<xcdg:DataGridControl.Resources>
<c:ColorConverter x:Key="colorConverter" />
<Style TargetType="{x:Type xcdg:DataCell}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsSelected, RelativeSource={RelativeSource AncestorType={x:Type xcdg:DataRow}}}" Value="False">
<Setter Property="xcdg:DataCell.Background" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Converter={StaticResource colorConverter}}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsDirty, RelativeSource={RelativeSource AncestorType={x:Type xcdg:DataRow}}}" Value="True">
<Setter Property="xcdg:DataCell.Background" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Converter={StaticResource colorConverter}}" />
</DataTrigger>
</Style.Triggers>
</Style>
</xcdg:DataGridControl.Resources>
Where my Colour converter looks a bit like this;
[ValueConversion(typeof(DataCell), typeof(Brush))]
public class ColorConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var cell = (DataCell)value;
var content = (string)cell.Content;
if (content == null || cell.ParentRow.IsSelected)
{
return DependencyProperty.UnsetValue;
}
if (content == "Unknown")
{
return new SolidColorBrush(Colors.LightYellow);
}
...
return DependencyProperty.UnsetValue;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
My only problem now is that when I scroll my background colours aren't updated when the DataRows are reused.
I can't find an event to hook into when a DataRow is reused either...

WPF Trigger not null

How to trigger an action in WPF when the Property is not null?
This is a working solution when is null:
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
</Style.Triggers>
I know that you cant "turn around" the condition and do what you need, but want to know
Unfortunately, you can't. But actually it's not necessary : you just need to specify the background for when the value is not null in the style setters, not in the trigger :
<Style.Setters>
<!-- Background when value is not null -->
<Setter Property="Background" Value="Blue" />
</Style.Setters>
<Style.Triggers>
<DataTrigger Binding="{Binding}" Value="{x:Null}">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
</Style.Triggers>
You can use DataTrigger class in Microsoft.Expression.Interactions.dll that come with Expression Blend.
Code Sample:
<i:Interaction.Triggers>
<ie:DataTrigger Binding="{Binding YourProperty}" Value="{x:Null}" Comparison="NotEqual">
<ie:ChangePropertyAction PropertyName="YourTargetPropertyName" Value="{Binding YourValue}"/>
</ie:DataTrigger>
</i:Interaction.Triggers>
Using this method you can trigger against GreaterThan and LessThan too.
In order to use this code you should reference two dll's:
System.Windows.Interactivity.dll
Microsoft.Expression.Interactions.dll
And add the corresponding namespaces:
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ie="http://schemas.microsoft.com/expression/2010/interactions"
it is an old question, but I want to answer. Actually you can. Just you have to use Converter in binding. Converter must return is null or not. So you will check statement is true or false. It provide you can check two condition if return value is false, it means it is not null. If it is true, it means it is null.
<converters:IsNullConverter x:Key="IsNullConverterInstance"/>
<Style>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=DataContext, Converter={StaticResource IsNullConverterInstance}" Value="True">
<Setter Property="Background" Value="Yellow" />
</DataTrigger>
</Style.Triggers></Style>
public class IsNulConverter: IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return Binding.DoNothing;
}
}

Resources