Styling DataGridCell correctly - wpf

This is a question following my previous problem, you can find it right there
So. Now I defined a DataGrid with a specific ElementStyle for each column (which just defines the TextBlocks inside in bold & white -- will come over this problem later)
So now I have two questions
First question (solved)
When I happen to set a background to my cell, it overrides the default style, and the background stays the same when the cell is highlighted.
One example of a style:
<!-- Green template for market-related -->
<ControlTemplate x:Key="Green" TargetType="{x:Type tk:DataGridCell}">
<Grid Background="Green">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
I'd naturally say that this is "normal" because I set the Grid's background to Green. I therefore tried it this way:
<!-- Light green template for sophis-related -->
<ControlTemplate x:Key="LightGreen" TargetType="{x:Type tk:DataGridCell}">
<Grid Background="LightGreen">
<Grid.Resources>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tk:DataGridCell}},
Converter={StaticResource DebugConverter}}" Value="True">
<Setter Property="Grid.Background" Value="#FF3774FF" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
This won't work either. As you can see I put a DebugConverter so I can check that the trigger is actually called, which is the case, but... Background does not change (and Snoop confirms this...)
Third try:
<!-- Light green template for sophis-related -->
<ControlTemplate x:Key="LightGreen" TargetType="{x:Type tk:DataGridCell}">
<ControlTemplate.Resources>
<Style TargetType="{x:Type tk:DataGridCell}">
<Setter Property="Background" Value="LightGreen" />
</Style>
</ControlTemplate.Resources>
<Grid>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
And... No background will be displayed (stays transparent)
So I think I am working in the wrong way here and I was wondering what should I do to JUST define the "not selected" template.
I would say that I may need to define a style BasedOn the "classic" style but, how would I do that? I tried to add TemplateBindings with no success
** EDIT: Solution**
As H B suggested in his answer, problem was coming from DependencyProperty Precedence, here's the solution:
<!-- Light green template for sophis-related -->
<ControlTemplate x:Key="LightGreen" TargetType="{x:Type tk:DataGridCell}">
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type Grid}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tk:DataGridCell}},
Converter={StaticResource DebugConverter}}" Value="True">
<Setter Property="Grid.Background" Value="#FF316AC5" />
</DataTrigger>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tk:DataGridCell}},
Converter={StaticResource DebugConverter}}" Value="False">
<Setter Property="Grid.Background" Value="LightGreen" />
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
</ControlTemplate>
Second question
Now, let's speak Triggers.
Basically, what I want to do is to define specific Triggers to my ElementStyle so the font color is white if the cell's background is Red or Green (the only aim of this is to have a better readability as Red and Green are kinda dark, black font on dark background results in a nice fail :p )
Edit Seems like I'm not clear enough: the following style is the style applied to each item of the datagrid, through the property DataGridTextColumn.ElementStyle. Here is the code handling that:
void VolatilityDataGrid_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
DataGridTextColumn column = e.Column as DataGridTextColumn;
column.ElementStyle = s_boldCellStyle;
// Other stuff here...
}
Here is what I do:
<!-- Cell style for colored matrix-->
<Style x:Key="BoldCellStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding Background, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tk:DataGridCell}}}"
Value="Red">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
<DataTrigger Binding="{Binding Background, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tk:DataGridCell}},
Converter={StaticResource DebugConverter}}"
Value="Green">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
And... It doesn't work. Strangely, what goes through converter is ONLY transparent background colors. I am definitely missing something here!
BTW, I also tried with classic triggers, no success either, I use DataTriggers here so I can debug the binding values!
Now I've been stuck for more than three days on this and I'm starting to freak out... Hopefully the Stackoverflow community will save me :)
Thanks!
Edit
Okay, update.
I understood why my Trigger does not work. The Background actually set is on the Grid and NOT on the DataGridCell. It is therefore normal that I don't get any color set there.
However, I ran some tests and found out that when the binding is set, the TextBlock does not have any parent yet (Parent = null). Binding to a RelativeSource of type Grid will bind me to... The whole DataGrid items presenter.
I'm not sure what to do now, since it seems like that from the actual TextBlock style I can't reach the parent Grid and therefore cannot resolve what color should I display according to the background.
Also, I can't change the Font color in my ControlTemplate because the DataGrid wants a Style for each column, which overrides the template's style by default (see my previous question and its answer)
So... Stuck again I am!

Dependency Property Value Precedence
This:
<Grid Background="LightGreen">
<Grid.Resources>
<Style TargetType="{x:Type Grid}">
<!-- Trigger Stuff -->
</Style>
</Grid.Resources>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
Needs to be:
<Grid>
<Grid.Resources>
<Style TargetType="{x:Type Grid}">
<Setter Property="Background" Value="LightGreen"/>
<!-- Trigger Stuff -->
</Style>
</Grid.Resources>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
Not sure about your second question as of now, possibly a related problem, i would suggest setting TextElement.Foreground instead of Foreground for starters. Getting Transparent as value is not very helpful, what control template do you use for the DataGridCell? If it is custom, is the Background hooked up properly via a TemplateBinding?
This works as long as the Background property is used, so if you have a ControlTemplate which sets things internally you need to externalize that. A normal DataGrid example:
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="LightGreen"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Content}" Value="Apple">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Content}" Value="Tomato">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding Background, RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}}" Value="Red">
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
<DataTrigger Binding="{Binding Background, RelativeSource={RelativeSource AncestorType={x:Type DataGridCell}}}" Value="Green">
<Setter Property="Foreground" Value="White"/>
</DataTrigger>
</Style.Triggers>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
So if the CellStyle sets the ControlTemplate the properties need to be hooked up via TemplateBinding. e.g.
<DataGrid.CellStyle>
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid Background="{TemplateBinding Background}">
<ContentPresenter />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="Background" Value="LightGreen"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Content}" Value="Apple">
<Setter Property="Background" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Content}" Value="Tomato">
<Setter Property="Background" Value="Green"/>
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.CellStyle>
Do not do the triggering inside the template or it will get messy.

Related

How to change Button Image when ListView is empty through a trigger

I would like to change the image of an WPF Button on ListView empty, but I don't now which is the property I must set from the trigger.
<Style x:Key="DisableOnEmptyLvStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=myListView, Path=Items.Count}" Value="0">
<Setter Property="IsEnabled" Value="False"/>
<!-- which button property I must set here? -->
</DataTrigger>
</Style.Triggers>
</Style>
<Button Style="{StaticResource DisableOnEmptyLvStyle}">
<Image Source="{StaticResource myImage}"/>
</Button>
Once ListView has items, then the image must change to the normal one.
Any ideas how to do this?
You should never set the Content directly using a Style, simply because the Style is instantiated once and potentially reused across multiple controls. The issue is that the value that you assign in the Setter for Content is also instantiated once and shared, but a control can only have a single parent in WPF.
What can go wrong? If you set or change the Content in a style and reference it for e.g. multiple Buttons you will see that only the last button shows the image. The first button gets its image set as Content, then the next button sets the image as Content and effectively removes the content of the first button.
There are two options that you can consider.
Create the images as resources and force each reference in XAML to get a new instance of the image by setting the x:Shared attribute to False.
When set to false, modifies WPF resource-retrieval behavior so that requests for the attributed resource create a new instance for each request instead of sharing the same instance for all requests.
<Window.Resources>
<!-- ...your image sources for "myImage" and "myImage". -->
<Image x:Key="myImageConrol" x:Shared="False" Source="{StaticResource myImage}"/>
<Image x:Key="myOtherImageConrol" x:Shared="False" Source="{StaticResource myImage}"/>
<Style x:Key="DisableOnEmptyLvStyle" TargetType="{x:Type Button}">
<Setter Property="Content" Value="{StaticResource myImageConrol}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=myListView, Path=Items.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Content" Value="{StaticResource myOtherImageConrol}">
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Button Style="{StaticResource DisableOnEmptyLvStyle}"/>
Create data templates and swap them out. This works because - as the name says - they are templates and its controls will be instantiated for each control they are applied to.
<Style x:Key="DisableOnEmptyLvStyle" TargetType="{x:Type Button}">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="{StaticResource myImage}"/>
</DataTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=myListView, Path=Items.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<Image Source="{StaticResource myOtherImage}"/>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Button Style="{StaticResource DisableOnEmptyLvStyle}"/>
Please try the following:
<Style x:Key="DisableOnEmptyLvStyle" TargetType="{x:Type Button}">
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource myImageAtLeastOne}"/>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding ElementName=myListView, Path=Items.Count}" Value="0">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource myImageEmpty}"/>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<Button Style="{StaticResource DisableOnEmptyLvStyle}" />

How to set image content to ContentControl dynamically using style?

I have custom Button which contains ContentControl named "IconBox", which content depends on triggers:
<Button>
<Button.Style>
<Style TargetType="{x:Type Button}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Grid>
<!-- somethig here ..... -->
<ContentControl x:Name="IconBox" HorizontalAlignment="Center"
VerticalAlignment="Center"
Visibility="Collapsed" />
</Grid>
<ControlTemplate.Triggers>
<!-- triggers here -->
</ControlTemplate.Triggers>
<!-- closing tags ... -->
</Button>
Most of the triggers looks like:
<DataTrigger Binding="{Binding Path=Flag}" Value="icon1">
<Setter TargetName="IconBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="IconBox" Property="Style"
Value="{DynamicResource icon1}"/>
</DataTrigger>
Where icon1 is a ControlTemplate with ViewBox and vector images inside.
So it's already known.
Now I need to add also bitmap images, which name contains in new Model's property let's call it Image.
So I want something like:
<DataTrigger Binding="{Binding Path=Flag}" Value="useImage">
<Setter TargetName="IconBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="IconBox" Property="Style"
Value="{DynamicResource ImageTemplate}"/>
</DataTrigger>
How then ImageTemplate should look like?
I've tried next one:
<Style TargetType="ContentControl" x:Key="ImageTemplate">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContentControl">
<Viewbox>
<Image>
<Image.Source>
<Binding RelativeSource="{RelativeSource Self}"
Converter="{StaticResource ImagePathConverter}"
Path="Image"/>
</Image.Source>
</Image>
</Viewbox>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
But it didn't work. Why?
Image contains sting like "image1.bmp", and ImagePathConverter simply adds full path to it - "%CurrentDir%\Resources\image1.bmp"
Breakpoint inside converter doesn't hit. As far as one inside Image getter.
And I have no idea why. Maybe some error happens inside my template or trigger?
The other triggers with Canvas inside ViewBox works fine.
Though when I'm using VisualTree to check this element properties in runtime, breakpoint inside Image getter hits.
Any other ideas how could I implement the image inside ImageBox?
If you define a DataTemplate:
<DataTemplate x:Key="ImageTemplate">
<Image Source="{Binding Image, Converter={StaticResource ImagePathConverter}}"/>
</DataTemplate>
...you could set the ContentTemplate of the ContentControl to this one:
<DataTrigger Binding="{Binding Path=Flag}" Value="useImage">
<Setter TargetName="IconBox" Property="Visibility" Value="Visible"/>
<Setter TargetName="IconBox" Property="ContentTemplate" Value="{DynamicResource ImageTemplate}"/>
<Setter TargetName="IconBox" Property="Content" Value="{Binding}"/>
</DataTrigger>
The Content property should be bound to the object where the Image property is defined.
Also note that your converter should return an ImageSource and not a string:
"Cannot convert string to ImageSource." How can I do this?

style triggers for alternating rows don't always update when scrolling virtualized WPF Datagrid

There are a few questions similar to this on SO, I've read this one and another I can't find the link for, but none of them seem to have a solution that's applicable.
I have a DataGrid defined below, and it has various styles triggered on AlternationIndex being either 0 or 1. When I scroll upwards, sometimes the a given cell will flip from one colour to the other.
Do you know of any way to stop this from happening without turning off virtualization?
(I've taken the column definitions out to save space, I don't think they're important for this. All of the DataTriggers -always- work, it's just the alternation that I'm having issues with.)
<DataGrid
ItemsSource="{Binding Path=LogItems, Mode=OneWay}"
Grid.Row="1"
AlternationCount="2"
Name="logDataGrid"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling">
<DataGrid.Resources>
<local:IsEntryExceptionConverter x:Key="isEntryExceptionConverter" />
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Border Name="DataGridCellBorder">
<ContentControl Content="{TemplateBinding Content}">
<ContentControl.ContentTemplate>
<DataTemplate>
<TextBlock Background="Transparent" TextWrapping="WrapWithOverflow" TextTrimming="CharacterEllipsis"
Height="auto" Width="auto" Text="{Binding Text}"/>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="DataGridCell.IsSelected" Value="True">
<Setter Property="TextBlock.Foreground" Value="Blue" />
</Trigger>
</Style.Triggers>
</Style>
<SolidColorBrush x:Key="ExceptionBrush" Color="OrangeRed" Opacity="0.5"/>
<SolidColorBrush x:Key="ErrorBrush" Color="Red" Opacity="0.5"/>
<SolidColorBrush x:Key="WarningBrush" Color="Orange" Opacity="0.5"/>
<SolidColorBrush x:Key="AlternatingRowBackground0" Color="AliceBlue" Opacity="0.5" />
<SolidColorBrush x:Key="AlternatingRowBackground1" Color="LightBlue" Opacity="0.5" />
</DataGrid.Resources>
<DataGrid.RowStyle>
<Style TargetType="DataGridRow">
<Style.Triggers>
<Trigger Property="AlternationIndex" Value="0">
<Setter Property="Background" Value="{StaticResource AlternatingRowBackground0}" />
</Trigger>
<Trigger Property="AlternationIndex" Value="1">
<Setter Property="Background" Value="{StaticResource AlternatingRowBackground1}" />
</Trigger>
<DataTrigger Binding="{Binding Path=Level}" Value="Warning">
<Setter Property="Background" Value="{StaticResource WarningBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Message, Converter={StaticResource isEntryExceptionConverter}}" Value="True">
<Setter Property="Background" Value="{StaticResource ExceptionBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=Level}" Value="Error">
<Setter Property="Background" Value="{StaticResource ErrorBrush}" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.RowStyle>
</DataGrid>
That's the nature of virtualization. Only a set number of UI objects are actually rendered, and when you scroll you are changing the DataContext behind those objects.
So in your case, Rows are created and given a background color. When you scroll, the DataContext behind those rows change, so a data object might be in a row that was given Color A at a certain scroll position, or be in a row that was assigned color B at another scroll position.
Most of the time the alternating colors are only there to help identify what columns are in what row so it doesn't matter if they change, however if you want to maintain a consistent background color for the rows you will probably have to add something to the data object and base your background color off that property. That way when you scroll and the DataContext changes, the row color will also change.

How to parametrize WPF Style?

I'm looking for a simplest way to remove duplication in my WPF code.
Code below is a simple traffic light with 3 lights - Red, Amber, Green. It is bound to a ViewModel that has one enum property State taking one of those 3 values.
Code declaring 3 ellipses is very duplicative. Now I want to add animation so that each light fades in and out - styles will become even bigger and duplication will worsen.
Is it possible to parametrize style with State and Color arguments so that I can have a single style in resources describing behavior of a light and then use it 3 times - for 'Red', 'Amber' and 'Green' lights?
<UserControl.Resources>
<l:TrafficLightViewModel x:Key="ViewModel" />
</UserControl.Resources>
<StackPanel Orientation="Vertical" DataContext="{StaticResource ViewModel}">
<StackPanel.Resources>
<Style x:Key="singleLightStyle" TargetType="{x:Type Ellipse}">
<Setter Property="StrokeThickness" Value="2" />
<Setter Property="Stroke" Value="Black" />
<Setter Property="Height" Value="{Binding Width, RelativeSource={RelativeSource Self}}" />
<Setter Property="Width" Value="60" />
<Setter Property="Fill" Value="LightGray" />
</Style>
</StackPanel.Resources>
<Ellipse>
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}" BasedOn="{StaticResource singleLightStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Red">
<Setter Property="Fill" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Ellipse>
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}" BasedOn="{StaticResource singleLightStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Amber">
<Setter Property="Fill" Value="Red" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Ellipse>
<Ellipse.Style>
<Style TargetType="{x:Type Ellipse}" BasedOn="{StaticResource singleLightStyle}">
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Green">
<Setter Property="Fill" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</StackPanel>
As long as your "Traffic Light" is wrapped up inside a control, which it appears it is, I don't think this is horrible. Each ellipse is well defined and has different triggers, each indicating its own state. You've already factored the common parts out into the base style, which is good.
You could wrap the individual ellipses inside another user control (which wouldn't need a backing ViewModel) that had an ActiveState property and an ActiveFill property. Then your TrafficLight looks something like:
<StackPanel Orientation="Vertical" DataContext="{StaticResource ViewModel}">
<my:Indicator State="{Binding State}" ActiveState="Red" ActiveFill="Red" />
<my:Indicator State="{Binding State}" ActiveState="Amber" ActiveFill="Red" />
<my:Indicator State="{Binding State}" ActiveState="Green" ActiveFill="Green" />
</StackPanel>
This lets you wrap up all your Ellipse styling inside your Indicator control and the only thing that control needs to worry about is comparing the State to the ActiveState to determine if it should fill itself with the ActiveFill brush.
As to if this is worth the effort or not, that depends on how many of these you have floating around and if you use them outside of your Traffic Light user control. Remember: You Ain't Gonna Need It.

Not able to Style my WPF Controls

I am going crazy here! What am I missing and why it is not styling anything:
<Style x:Key="textBoxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
<TextBox Width="100" Style="{StaticResource textBoxStyle}" Height="20" Background="Yellow" ></TextBox>
The above code does not do anything. It does not highlight the TextBox control!
This occurs because local values override style values. (Properties set directly on an element have very high precedence.) You are setting Background directly on the TextBox, so WPF is going, "Well, he normally wants textBoxStyle backgrounds to be Red when focused, but for this particular TextBox, he's said he specifically wants Background to be Yellow, so Yellow it is."
So the fix is to move the Yellow background to be part of the Style:
<Style x:Key="textBoxStyle" TargetType="{x:Type TextBox}">
<Setter Property="Background" Value="Yellow" />
<Style.Triggers>
<Trigger Property="IsFocused" Value="True">
<Setter Property="Background" Value="Red" />
</Trigger>
</Style.Triggers>
</Style>
and remove it from the TextBox:
<TextBox Width="100" Style="{StaticResource textBoxStyle}" Height="20" />
Define your Style before the TextBox or use DynamicResource instead of StaticResource

Resources