DataTrigger in ControlTemplate breaks MultiBinding - wpf

I have following ControlTemplate which works fine
<ControlTemplate x:Key="TotalCostsStatisticTemplate">
<StackPanel x:Name="ContentHolderPanel" Visibility="Collapsed"
Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock x:Name="ValueTextBlock" VerticalAlignment="Center"
Style="{DynamicResource PhasingValueTextStyle}">
<TextBlock.Text>
<MultiBinding Converter="{ttConverters:CustomDisplayFormatConverter}">
<Binding Path="FormatSettings" />
<Binding Path="AvailableStatistics.CostsFormat"/>
<Binding Path="TotalCosts" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ShowTotalCosts}" Value="True">
<Setter TargetName="ContentHolderPanel" Property="Visibility" Value="Visible"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
But if I add another DataTrigger then the MultiBinding on TextBlock.Text doesn't work when control using this template is loaded for first time i.e. the converter CustomDisplayFormatConverter fires only once with all values as UnsetValue and doesn't fire again (it works fine on reloading the window again).
<ControlTemplate x:Key="TotalCostsStatisticTemplate">
<StackPanel x:Name="ContentHolderPanel" Visibility="Collapsed"
Orientation="Horizontal" HorizontalAlignment="Center">
<TextBlock x:Name="ValueTextBlock" VerticalAlignment="Center"
Style="{DynamicResource PhasingValueTextStyle}">
<TextBlock.Text>
<MultiBinding Converter="{ttConverters:CustomDisplayFormatConverter1}">
<Binding Path="FormatSettings" />
<Binding Path="AvailableStatistics.CostsFormat"/>
<Binding Path="TotalCosts" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</StackPanel>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding ShowTotalCosts}" Value="True">
<Setter TargetName="ContentHolderPanel" Property="Visibility" Value="Visible"/>
</DataTrigger>
<!--Trigger causing problem (breaking TextBlock.Text multi binding on first load) -->
<DataTrigger Binding="{Binding IsCostsComplete}" Value="False">
<Setter TargetName="ValueTextBlock" Property="Foreground"
Value="{DynamicResource ManagerErrorBrush}"/>
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
If I place this DataTrigger inside TextBlock.Style it works fine!
Any idea what's wrong?
Update:
Here's how its getting used
<ControlTemplate x:Key="PhasingStatisticValuesTemplate">
<StackPanel x:Name="ContentHolderPanel" Orientation="Horizontal" Visibility="{Binding IsValid, Converter={StaticResource boolToVisibilityConverter}}">
<!-- Other control elements based on various templates -->
<Control Margin="20,10" Template="{DynamicResource TotalCostsStatisticTemplate}"/>
</StackPanel>
</ControlTemplate>
This PhasingStatisticValuesTemplate is used in another ControlTemplate, which is then used inside a DataTemplate, so there is a long hierarchy of ControlTemplates -
<ControlTemplate x:Key="PhasingStatisticsTemplate">
<Grid>
<ScrollViewer Style="{DynamicResource CompactHorizontalScrollViewerStyle}" Name="ScrollContainer">
<Grid Name="ScrollViewerGrid" Background="Transparent">
<ContentControl Template="{DynamicResource PhasingStatisticValuesTemplate}" Name="ScrollViewerContent" />
</Grid>
</ScrollViewer>
</Grid>
</ControlTemplate>
Another useful info. might be that existing style on that TextBlock also updated Foreground, not sure how that can cause this behavior -
<Style x:Key="PhasingValueTextStyle" TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding IsComplete}" Value="False">
<Setter Property="Foreground" Value="{DynamicResource ManagerErrorBrush}"/>
</DataTrigger>
</Style.Triggers>
</Style>

Related

TextBlock style triggers

I would like to combine the DisplayNames from two different ViewModels, but only IF the first is not equal to a NullObject.
I could easily do this in either a converter or a parent view model, but am
This displays nothing at all:
<TextBlock Grid.Column="2" Grid.Row="0" >
<TextBlock.Inlines>
<Run Text="{Binding HonorificVm.DisplayName}"/>
<Run Text="{Binding PersonNameVm.DisplayName}"/>
</TextBlock.Inlines>
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Style.Triggers>
<DataTrigger Binding="{Binding HonorificVm.Honorific}" Value="{x:Static model:Honorific.NullHonorific}">
<Setter Property="Text" Value="PersonNameVm.DisplayName"/>
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
Why?
I would split it into two TextBlocks and only change the visibility using a trigger. By using the Inlines and trying to change the Text in the triggers you probably run into precedence problems and the Inlines cannot be extracted to a Setter.
e.g.
<StackPanel Grid.Column="2" Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="{Binding HonorificVm.DisplayName}" Margin="0,0,5,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<DataTrigger Binding="{Binding HonorificVm.Honorific}"
Value="{x:Static model:Honorific.NullHonorific}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding PersonNameVm.DisplayName}" />
</StackPanel>
An alternative would be a MultiBinding instead of Inlines:
<TextBlock Grid.Column="2" Grid.Row="0">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Text">
<Setter.Value>
<MultiBinding StringFormat="{}{0} {1}">
<Binding Path="HonorificVm.DisplayName" />
<Binding Path="PersonNameVm.DisplayName" />
</MultiBinding>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding HonorificVm.Honorific}"
Value="{x:Static model:Honorific.NullHonorific}">
<Setter Property="Text" Value="{Binding PersonNameVm.DisplayName}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>

Dynamic StringFormat in WPF

I have the following TextBlock Style:
<Style TargetType="TextBlock" x:Key="MyValues">
<Setter Property="FontStyle" Value="Italic"/>
<Setter Property="Foreground" Value="DarkBlue"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsMetric}" Value="True">
<Setter Property="Text">
<Setter.Value>
<MultiBinding StringFormat="F1">
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=IsMetric}" Value="False">
<Setter Property="Text">
<Setter.Value>
<MultiBinding StringFormat="F3">
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Green"/>
</DataTrigger>
</Style.Triggers>
I then use TextBlocks as follows:
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=Breadth}" Style="{StaticResource MyValues}"/>
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Depth}" Style="{StaticResource MyValues}"/>
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding Path=Area}" Style="{StaticResource MyValues}" />
The intention is to set the StringFormat depending on a bound property IsMetric. The Binding in the style are left empty because i want to apply the same style for multiple TextBlocks all bound to different properties. The triggers are working but the StringFormat` is ignored, any ideas?
Here you set Text property to be different things in TextBlock declaration and in DataTriggers. In the first case it's an instance of Binding class. In the second case it's an instance of MultiBinding class. Finally it is one of these. It cannot be both at the moment.
The following markup
<Setter Property="Text">
<Setter.Value>
<MultiBinding StringFormat="F1">
</MultiBinding>
</Setter.Value>
</Setter>
instantiates MultiBinding class instance and sets it to Text property.
The Text="{Binding Path=Breadth}" instantiates Binding class instance and sets it to Text property.

WPF Style Triggers in DataTemplate

i hope you can help me. I got following Code in the Resources:
<UserControl.Resources>
<BitmapImage x:Key="img_src_lock" UriSource="/EEBase;component/Images/Lock_24x32.png" />
<BitmapImage x:Key="img_src_unlock" UriSource="/EEBase;component/Images/Unlock_24x32.png" />
<Style TargetType="{x:Type ToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="True">
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource img_src_lock}" />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="False">
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource img_src_unlock}" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
<!-- TypeComboTemplateCollapsed -->
<DataTemplate x:Key="TypeComboTemplateCollapsed">
<TextBlock
Text="{Binding Path=Text, Mode=OneWay}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="5,0,0,5"
/>
</DataTemplate>
<!-- TypeComboTemplateExpanded -->
<DataTemplate x:Key="TypeComboTemplateExpanded">
<TextBlock
Text="{Binding Path=Text, Mode=OneWay}"
VerticalAlignment="Center"
Margin="5,0,0,5"
/>
</DataTemplate>
<!-- EditCircleTemplate -->
<DataTemplate x:Key="EditCircleTemplate">
<!-- some content here, no ToggleButton -->
</DataTemplate>
<!-- EditRectangleTemplate -->
<DataTemplate x:Key="EditRectangleTemplate">
<!-- some other content here, including the ToggleButtons -->
<ToggleButton
IsChecked="{Binding Path=BaseLocked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
Margin="5"
/>
<ToggleButton
IsChecked="{Binding Path=HeightLocked, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=True}"
Margin="5"
/>
</DataTemplate>
</UserControl.Resources>
To me that looks all correct.
Now, the problem is:
When i do the following, an exceptions occurs:
Specified element is already the logical child of another element. Disconnect it first.
1. load the control, selected type is CIRC
2. change the dropdown to select RECT (template triggers and togglebuttons are shown correctly)
3. change the dropdown back to CIRC
--> now the Exception occurs.
4. if i ignore the exception, the template "EditCircleTemplate" does not get loaded, and the normal ToString of the model object gets displayed.
Additional info:
originally there are 4 different types in the WPF, two of them with ToggleButtons (that's why i use templates). I cut them out, they dont differ really. But what i found out using all 4 templates is that the error does not occur when switching to a new Template, but when unloading a template with the toggle buttons. Which is kinda strange.
Also if i remove the DataTriggers for the ToggleButtons everything works like a charm.
The Exception comes from the XAML-Interpreter, so the Stacktrace is not useful at all.
Can anyone give me a hint what i am doing wrong?
Edit:
oops, i guess i forgot the content code:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ComboBox
Margin="5"
Grid.Row="0"
Grid.Column="0"
ItemsSource="{Binding Path=PossibleTypes, Mode=OneTime}"
SelectedItem="{Binding Path=SelectedType, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<ContentControl x:Name="content" Content="{Binding}" ContentTemplate="{StaticResource TypeComboTemplateExpanded}"/>
</StackPanel>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=ComboBoxItem}}" Value="{x:Null}">
<Setter TargetName="content" Property="ContentTemplate" Value="{StaticResource TypeComboTemplateCollapsed}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ContentControl
Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="2"
Content="{Binding Mode=OneWay}">
<ContentControl.ContentTemplate>
<DataTemplate >
<ContentControl
Name="inputContent"
Content="{Binding Mode=OneWay}"
ContentTemplate="{x:Null}"
/>
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding Path=SelectedType.Type, Mode=OneWay}"
Value="CIRC">
<Setter
TargetName="inputContent"
Property="ContentTemplate"
Value="{StaticResource EditCircleTemplate}"
/>
</DataTrigger>
<DataTrigger
Binding="{Binding Path=SelectedType.Type, Mode=OneWay}"
Value="RECT">
<Setter
TargetName="inputContent"
Property="ContentTemplate"
Value="{StaticResource EditRectangleTemplate}"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ContentControl.ContentTemplate>
</ContentControl>
</Grid>
Edit2/Solution:
I just found a workaround - it just does not satisfy me:
Instead of putting the style in the UserControl.Resources, which for me would be the more clean and intuitive solution, i have to set the style with the triggers on each ToggleButton separately.
So removing the and adding following code to each ToggleButton did the trick:
<ToggleButton.Style>
<Style TargetType="{x:Type ToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="True" >
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource img_src_lock}" />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="False">
<Setter Property="Content">
<Setter.Value>
<Image Source="{StaticResource img_src_unlock}" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</ToggleButton.Style>
The big question still persists: WHY?
The problem is that if you create an Image in a style there is only one instance, so as soon as multiple controls use the style there is going to be a fight over this one instance which can only be owned by one control.
The easiest solution to this is placing the Style in a resource and setting x:Shared to false, that way a copy of the style is used whereever referenced.
Why do you need to create BitmapImages and set them as Source to your Content Images in Triggers? Why arent you using the URI source to Images directly?
<Style TargetType="{x:Type ToggleButton}">
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="True">
<Setter Property="Content">
<Setter.Value>
<Image Source="/EEBase;component/Images/Lock_24x32.png" />
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=IsChecked}" Value="False">
<Setter Property="Content">
<Setter.Value>
<Image Source="/EEBase;component/Images/Unlock_24x32.png" />
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
</Style>
Let me know if this helps.

WPF Display TextBlock with validation error message below control

Is there a way to display the error contents in a TextBlock below the control similar to how the following sets the Tooltip to contain the error text?
<Style x:Key="textBoxInError" TargetType="Control">
<Setter Property="Validation.ErrorTemplate">
<Setter.Value>
<ControlTemplate>
<DockPanel>
<TextBlock DockPanel.Dock="Left" Foreground="Red" FontWeight="Bold">*</TextBlock>
<TextBlock Text="WOULD LIKE TO SHOW WHAT TOOLTIP IS SHOWING" DockPanel.Dock="Bottom" Foreground="Red"/>
<Border BorderBrush="Red" BorderThickness="2">
<AdornedElementPlaceholder/>
</Border>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={x:Static RelativeSource.Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
In other words, I rather show the error message in a TextBlock below the control instead of a Tool Tip.
The DataContext of the ErrorTemplate is already the value of Validation.Errors, so you can just do:
<TextBlock Text="{Binding [0].ErrorContent}" DockPanel.Dock="Bottom" Foreground="Red"/>
or
<TextBlock Text="{Binding ErrorContent}" DockPanel.Dock="Bottom" Foreground="Red"/>
I struggled with this exact problem, and all the SO answers I could find to this or to similar questions were either using the "Validation.ErrorTemplate" property which is unfortunately rendered on a separate UI layer which means the parent control won't resize its content like #statikuz mentionned and hide the following controls, or just not generic enough.
I finally ended up with this solution :
<Style x:Key="hiddenTextblockErrorText" TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Foreground" Value="DarkRed"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="Text" Value="{Binding RelativeSource={RelativeSource Self}, Path=Tag.(Validation.Errors)/ErrorContent}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Tag.(Validation.HasError)}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
And you use it like this :
<TextBox Name="someField" Height="20">
<TextBox.Text>
<Binding Path="SomeProperty" Mode="TwoWay"UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:IsRequired/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Tag="{Binding ElementName=someField}" Style="{StaticResource hiddenTextblockErrorText}"/>
So you basically just bind your field to the Tag property of your error TextBlock, and you can use it from there to get the (Validation.Errors) attached property.
Note that you will get an harmless warning "Property Errors is not attachable to element of type Object" but it is working perfectly fine (I wasn't able to find how to do a cast here).
Alternatively you can use a Label and the Target property instead of Tag and you won't get any warning, but you lose the TextWrapping feature unless you also override the template so it's a bit more verbose :
<Style x:Key="hiddenLabelErrorText" TargetType="Label">
<Setter Property="Visibility" Value="Collapsed"/>
<Setter Property="Foreground" Value="DarkRed"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Label}">
<TextBlock TextWrapping="Wrap" Text="{Binding RelativeSource={RelativeSource AncestorLevel=1, AncestorType={x:Type Label}},
Path=Target.(Validation.Errors)/ErrorContent}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Target.(Validation.HasError)}" Value="True">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
<Label Target="{Binding ElementName=someField}" Style="{StaticResource hiddenLabelErrorText}"/>

WPF Trigger for IsSelected in a DataTemplate for ListBox items

I have a listbox, and I have the following ItemTemplate for it:
<DataTemplate x:Key="ScenarioItemTemplate">
<Border Margin="5,0,5,0"
Background="#FF3C3B3B"
BorderBrush="#FF797878"
BorderThickness="2"
CornerRadius="5">
<DockPanel>
<DockPanel DockPanel.Dock="Top"
Margin="0,2,0,0">
<Button HorizontalAlignment="Left"
DockPanel.Dock="Left"
FontWeight="Heavy"
Foreground="White" />
<Label Content="{Binding Path=Name}"
DockPanel.Dock="Left"
FontWeight="Heavy"
Foreground="white" />
<Label HorizontalAlignment="Right"
Background="#FF3C3B3B"
Content="X"
DockPanel.Dock="Left"
FontWeight="Heavy"
Foreground="White" />
</DockPanel>
<ContentControl Name="designerContent"
Visibility="Collapsed"
MinHeight="100"
Margin="2,0,2,2"
Content="{Binding Path=DesignerInstance}"
Background="#FF999898">
</ContentControl>
</DockPanel>
</Border>
</DataTemplate>
As you can see the ContentControl has Visibility set to collapsed.
I need to define a trigger that causes the Visibility to be set to "Visible"
when the ListItem is selected, but I can't figure it out.
Any ideas?
UPDATE: Of course I could simply duplicate the DataTemplate and add triggers
to the ListBox in question to use either one or the other, but I want to prevent duplicating this code.
You can style your ContentControl such that a trigger fires when its container (the ListBoxItem) becomes selected:
<ContentControl
x:Name="designerContent"
MinHeight="100"
Margin="2,0,2,2"
Content="{Binding Path=DesignerInstance}"
Background="#FF999898">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger
Binding="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}},
Path=IsSelected}"
Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
Alternatively, I think you can add the trigger to the template itself and reference the control by name. I don't know this technique well enough to type it from memory and assume it'll work, but it's something like this:
<DataTemplate x:Key="ScenarioItemTemplate">
<DataTemplate.Triggers>
<DataTrigger
Binding="{Binding
RelativeSource={RelativeSource
Mode=FindAncestor,
AncestorType={x:Type ListBoxItem}},
Path=IsSelected}"
Value="True">
<Setter
TargetName="designerContent"
Property="Visibility"
Value="Visible"/>
</DataTrigger>
</DataTemplate.Triggers>
...
</DataTemplate>
#Matt, Thank you!!!
Just had to add a trigger for IsSelected == false as well,
and now it works like a charm!
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ListBoxItem}},Path=IsSelected}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor,AncestorType={x:Type ListBoxItem}},Path=IsSelected}" Value="False">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>

Resources