INotifyPropertyChange ignored in binding to a DataTrigger - wpf

I'm working on some code that has a Button that contains an image and some text, and which should display either the image, the text, or both, depending upon the value of a bound property. The code is currently using Styles and DataTriggers:
public enum ButtonStyle { Image, Text, Both };
public class ViewModel : INotifyPropertyChanged
{
private ButtonStyle _buttonStyle;
public ButtonStyle buttonStyle
{
get { return this._buttonStyle; }
set
{
this._buttonStyle = value;
notifyPropertyChanged("buttonStyle");
}
}
And:
<UserControl.Resources>
<Style x:Key="buttonTextStyle" TargetType="{x:Type Label}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=buttonStyle}"
Value="{x:Static local:ButtonStyle.Text}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="buttonImageStyle" TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=buttonStyle}"
Value="{x:Static local:ButtonStyle.Image}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</UserControl.Resources>
<Button>
<StackPanel>
<Image Source="..." Style="{StaticResource buttonImageStyle} />
<Label Style={StaticResource buttonTextStyle}>My Text</Label>
</StackPanel>
</Button>
My problem? The button doesn't change when I change the value of the buttonStyle property in the view model. This control is in a tab, and if I switch to another tab and then switch back, the button updates to reflect the current value of the buttonStyle property, but it does not change until I do.
It looks like the DataTrigger is processed only when the control is rendered, and does not re-render when the bound value is modified, despite the bound value raising a PropertyChanged event.
Any ideas?

Try NotifyOnSourceUpdated=True on each of your data triggers.
<DataTrigger Binding="{Binding Path=buttonStyle, NotifyOnSourceUpdated=True}"
Value="{x:Static local:ButtonStyle.Text}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>

I think this is a nicer way to refer to enums in your DataTrigger:
<Style x:Key="buttonImageStyle" TargetType="{x:Type Image}">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=buttonStyle}">
<DataTrigger.Value>
<local:ButtonStyle>Text</local:ButtonStyle>
</DataTrigger.Value>
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
The value of the resource changes during runtime, thats why you should use DynamicResource instead of StaticResource:
Style="{DynamicResource buttonImageStyle}"

Here's an idea - any time you have a binding problem and it looks like INotifyPropertyChanged isn't working, check and double check and make damned sure that you spelled the name of the property right, in your PropertyChangedEventArgs().
Sorry for the trouble.

Related

WPF: unique style for each combo box item bound to dictionary

A ComboBox bound to the dictionary (of Enum, String). Selected value path is dictionary's Key.
Is it possible to set individual style for each combo box item in XAML?
In the following I use a custom enum called Cards, that has constants Skull, Hearts and others for demonstration purposes. You can simply use your enum type instead.
Item Container Style Using Data Triggers
You could create an items container style with triggers for each enum value.
<Style x:Key="EnumComboBoxItemStyle" TargetType="{x:Type ComboBoxItem}">
<Style.Triggers>
<DataTrigger Binding="{Binding Key}" Value="{x:Static local:Cards.Skull}">
<Setter Property="Foreground" Value="Blue"/>
</DataTrigger>
<DataTrigger Binding="{Binding Key}" Value="{x:Static local:Cards.Hearts}">
<Setter Property="Foreground" Value="Red"/>
</DataTrigger>
</Style.Triggers>
</Style>
<ComboBox ItemContainerStyle="{StaticResource EnumComboBoxItemStyle}" ...>
Item Container Style Selector
Another option is to create a style selector if you have multiple distinct styles like this:
<Style x:Key="SkullComboBoxItemStyle" TargetType="{x:Type ComboBoxItem}">
<Setter Property="Foreground" Value="Green"/>
</Style>
<Style x:Key="HeartsComboBoxItemStyle" TargetType="{x:Type ComboBoxItem}">
<Setter Property="Foreground" Value="Red"/>
</Style>
<!-- ...other styles. -->
The style selector determines the style based on the enum value.
public class CardsKeyStyleSelector : StyleSelector
{
public override Style SelectStyle(object item, DependencyObject container)
{
if (container is FrameworkElement element && item is KeyValuePair<Cards, string> keyValuePair)
{
switch (keyValuePair.Key)
{
case Cards.Skull:
return element.FindResource("SkullComboBoxItemStyle") as Style;
case Cards.Hearts:
return element.FindResource("HeartsComboBoxItemStyle") as Style;
// ...other cases.
}
}
return null;
}
}
You can assign the style selector to your combo box and it will choose the right style.
<ComboBox ...>
<ComboBox.ItemContainerStyleSelector>
<local:CardsKeyStyleSelector/>
</ComboBox.ItemContainerStyleSelector>
</ComboBox>
My solution is further. First, define enumerable type and dictionary, associated with it:
Public Enum MyEnum As Integer
OneValue = 0
OtherValue = 1
End Enum
Public ReadOnly Property MyDict As Dictionary(Of MyEnum, String)
Get
Return New Dictionary(Of MyEnum, String) From {
{MyEnum.OneValue, "First value text"},
{MyEnum.OtherValue, "Other value text"}
}
End Get
End Property
Next, in XAML use it in this manner:
<Style TargetType="Ellipse" x:Key="ellipseStyle">
<Setter Property="Height" Value="10" />
<Setter Property="Width" Value="10" />
<Style.Triggers>
<DataTrigger Binding="{Binding Key}" Value="0"><!-- "0" - one of the dictionary key -->
<Setter Property="Fill" Value="Red" />
</DataTrigger>
<DataTrigger Binding="{Binding Key}" Value="1"><!-- "1" - one of the dictionary key -->
<Setter Property="Fill" Value="Green" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style TargetType="{x:Type ComboBox}" x:Key="cmb_osn_rez">
<Setter Property="ItemsSource" Value="{Binding MyDict}" />
<Setter Property="SelectedValuePath" Value="Key" />
<Setter Property="ItemsControl.ItemTemplate">
<Setter.Value>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Ellipse Style="{StaticResource ellipseStyle}" />
<TextBlock Text="{Binding Value}" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</Style>

Switch images with a data template

I know that already some questions exist but I can not fix my problem with them.
Problem: I try to change a image with a data template but just the default image is visible.
Code:
My xaml code is like this:
<Window.Resources>
<DataTemplate x:Key="MultiTemplate">
<Image Height="17" Width="17">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Source" Value="{svg2Xaml:SvgImage VideoControllerTester;component/Resources/Start.svg}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MultiTrigger}" Value="start">
<Setter Property="Source" Value="{svg2Xaml:SvgImage VideoControllerTester;component/Resources/Start.svg}"/>
</DataTrigger>
<DataTrigger Binding="{Binding MultiTrigger}" Value="stop">
<Setter Property="Source" Value="{svg2Xaml:SvgImage VideoControllerTester;component/Resources/Stop.svg}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>
</DataTemplate>
</Window.Resources>
<ContentControl ContentTemplate="{DynamicResource MultiTemplate}"/>
In the code behind I set MultiTrigger = "start" or "stop".
Question: Can I show the images with the content control? Or I do some dumb stuff with the data template?
Edit:
public string MultiTrigger
{
get { return _multiTrigger; }
set
{
_multiTrigger = value;
RaisePropertyChanged();
}
}
Assuming that there is a MainViewModel class with a MultiTrigger property (which btw. is a strange property name), you would assign an instance of the view model class to the MainWindow's DataContext, either in code behind:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
Or in XAML:
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
Then you would declare the Image Style as a resource:
<Window.Resources>
<Style TargetType="Image" x:Key="ImageStyle">
<Setter Property="Source" Value="{svg2Xaml:SvgImage VideoControllerTester;component/Resources/Start.svg}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding MultiTrigger}" Value="stop">
<Setter Property="Source" Value="{svg2Xaml:SvgImage VideoControllerTester;component/Resources/Stop.svg}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Window.Resources>
and apply it to an Image control:
<Image Style="{StaticResource ImageStyle}"/>
Then change the property value somewhere in the MainWindow's code behind by directly accessing the view model instance like this:
((MainViewModel)DataContext).MultiTrigger = "stop";

How to bind data from View to ViewModel?

I have little knowledge about mvvm, but this is how I wrote my code this far:
<Image x:Name ="new_tooltip" Grid.Row="84" Grid.Column="57" Grid.ColumnSpan="78" Grid.RowSpan="15" Source="/MS_Show_Assets/MainMenuAssets/TT-Startscreen-MainMenu-New-DE.png" Visibility = "{Binding IsMouseOver, ElementName=New, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Visibility" Value="{Binding Path=IsNewTooltipVisible, Mode=OneWayToSource}" />
</Style>
</Image.Style>
</Image>
and ViewModel:
public class ViewMainMenuViewModel : BindableBase
{
public string IsNewTooltipVisible { get; set; }
public ViewMainMenuViewModel()
{
}
}
So basically, I want some image in the view to become visible if the mouse is over some button. Then once this image is visible, I want to send "Visible" to a property that is in ViewModel class. What else am I missing in this class?
you dont need a property in VM to do this. You can use Trigger on View itself to show/hide image on button mouseover as below. Here ElementName is the name of the button whose mouseover you want to capture.
<Image x:Name ="new_tooltip" Grid.Row="84" Grid.Column="57" Grid.ColumnSpan="78" Grid.RowSpan="15" Source="/MS_Show_Assets/MainMenuAssets/TT-Startscreen-MainMenu-New-DE.png" Visibility = "{Binding IsMouseOver, ElementName=New, Converter={StaticResource BooleanToVisibilityConverter}}">
<Image.Style>
<Style TargetType="{x:Type Image}">
<Setter Property="Visibility" Value="Collapsed"></Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsMouseOver, ElementName=myButton}" Value="true">
<Setter Property="Visibility" Value="Visible"></Setter>
</DataTrigger>
</Style.Triggers>
</Style>
</Image.Style>
</Image>

Why can't I add a DataTrigger to my control's Triggers collection?

Why cant I code like this
<Border Width="130" Height="70">
<Border.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="0">
<Setter Property="Style" Value="{StaticResource ResourceKey=ListBoxItemBorder}"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="200">
<Setter Property="Style" Value="{StaticResource ResourceKey=ListBoxItemBorderInactive}"/>
</DataTrigger>
</Border.Triggers>
</Border>
I get this error
Failed object initialization (ISupportInitialize.EndInit).
Triggers collection members must be of type EventTrigger.
Error at object '4_T' in markup file
What am I doing wrong plz help.
Abe is correct and explains the limitations well. One thing you might want to consider is:
Instead of having two border styles, and trying to pick between them based on a trigger...
Use a single style on your border, this style's setters represent your 'normal' look.
This style also contains your DataTrigger, and your DataTrigger has a collection of setters which essentially represents your second style (which have higher priority than the standard setters when this trigger evaluates to true!
Edit:
Something like this -
<Style TargetType="Border" x:Key="BorderStyle">
<!-- These setters are the same as your normal style when none of your triggers are true -->
<Setter Property="BorderBrush" Value="Black" />
<Style.Triggers>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="0">
<!-- These setters are the same as your ListBoxItemBorder style -->
<Setter Property="BorderBrush" Value="Green" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=CurrentStatus}" Value="200">
<!-- These setters are the same as your ListBoxItemBorderInactive style -->
<Setter Property="BorderBrush" Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
Unfortunately, only EventTriggers can be applied directly to elements. If you want to use a Trigger or DataTrigger, they have to be in a Style, ControlTemplate, or DataTemplate.
From the resource names, it looks like this is a Border inside a ListBoxItem ControlTemplate. You could easily move the triggers into the template's triggers collection.
Here is a way for no limitations triggers.
Example:
<Border Width="130" Height="100" Grid.Row="1">
<ListBox x:Name="lstItems" ItemsSource="{Binding TestItems}">
</ListBox>
<tg:TriggerExtensions.Triggers>
<tg:TriggerCollections>
<tg:DataTriggerInfo Binding="{Binding CurrentStatus}" Value="0">
<tg:DataTriggerInfo.Setters>
<tg:SetterInfo ElementName="lstItems" Property="Style" Value="{StaticResource ListBoxRed}"/>
</tg:DataTriggerInfo.Setters>
</tg:DataTriggerInfo>
<tg:DataTriggerInfo Binding="{Binding CurrentStatus}" Value="0" IsInvert="True">
<tg:DataTriggerInfo.Setters>
<tg:SetterInfo ElementName="lstItems" Property="Style" Value="{StaticResource ListBoxBlue}"/>
</tg:DataTriggerInfo.Setters>
</tg:DataTriggerInfo>
</tg:TriggerCollections>
</tg:TriggerExtensions.Triggers>
</Border>
Link Sample
Link Component Github

Using custom dependency properties as DataTrigger in WPF

I have a custom dependency property that I would like to use as a data trigger. Here is the code behind:
public static readonly DependencyProperty BioinsulatorScannedProperty =
DependencyProperty.Register(
"BioinsulatorScanned",
typeof(bool),
typeof(DisposablesDisplay),
new FrameworkPropertyMetadata(false));
public bool BioinsulatorScanned
{
get
{
return (bool)GetValue(BioinsulatorScannedProperty);
}
set
{
SetValue(BioinsulatorScannedProperty, value);
}
}
I have created a style and control template. My goal is to change the color of some text when the dependency prop is set to true...
<Style x:Key="TreatEye" TargetType="Label">
<Setter Property="Foreground" Value="#d1d1d1" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="30" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Label">
<Canvas>
<TextBlock x:Name="bioinsulatorText"
Canvas.Left="21" Canvas.Top="33"
Text="Bioinsulator" />
<TextBlock Canvas.Left="21" Canvas.Top="70"
Text="KXL Kit" />
</Canvas>
<ControlTemplate.Triggers>
<DataTrigger Binding="{Binding BioinsulatorScanned}"
Value="True">
<Setter TargetName="bioinsulatorText"
Property="Foreground" Value="Black" />
</DataTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Despite successfully setting the dependency prop to true programmatically, This trigger condition never fires. This is a real pain to debug!
Thanks in advance.
In this case I am switching the visibility of a button using a datatrigger based on a dependency property FirstLevelProperty.
public static readonly DependencyProperty FirstLevelProperty = DependencyProperty.Register("FirstLevel", typeof(string), typeof(MyWindowClass));
public string FirstLevel
{
get
{
return this.GetValue(FirstLevelProperty).ToString();
}
set
{
this.SetValue(FirstLevelProperty, value);
}
}
You can reference the dependency property FirstLevel(Property) contained (in this case) in a window by using the RelativeSource binding. Also you should set the default setting in the style, that will be overridden by the datatrigger.
<Button.Style>
<Style TargetType="Button">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=FirstLevel,
RelativeSource={RelativeSource FindAncestor,
AncestorType={x:Type Window}}}"
Value="SomeValue">
<Setter Property="Visibility"
Value="Hidden" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Visibility" Value="Visible" />
</Style>
</Button.Style>
It looks like your dependency property is defined inside a DisposableDisplay object that you created. In order for the binding specified to work, an instance of that DisposableDisplay object must be set as the DataContext of the control (label in this case) or any of its ancestors.

Resources