Changing style from inside a DataTrigger
Asked Answered
J

2

16

As often happens with WPF, I'm probably going about things the wrong way, but I have the following situation:

I would like to apply a style depending on a DataTrigger, but you can't change Style from inside a Style:

<Button>
   <Button.Style>
      <Style BasedOn="SomeStyle">
         <Style.Triggers>
            <DataTrigger ...>
               <Setter Property="Style" Value="OtherStyle" /> <---- NO CAN DO

Logical really, but what I want to avoid is duplication of the same setters, just because my trigger conditions change:

<Button>
   <Button.Style>
      <Style BasedOn="SomeStyle">
         <Style.Triggers>
            <DataTrigger Binding="{Binding X}" Value="Condition1">
               <Setter Property="A" Value="1" /> 
               <Setter Property="B" Value="1" /> 
               <etc...>
(...)

<Button>
   <Button.Style>
      <Style BasedOn="SomeStyle">
         <Style.Triggers>
            <DataTrigger Binding="{Binding X}" Value="Condition2">
               <Setter Property="A" Value="1" /> 
               <Setter Property="B" Value="1" /> 
               <etc...>

Is there something else inside which I could put the DataTrigger, thus allowing me to change the style from inside it? Or another way of: avoiding duplicating style info?

Junette answered 10/9, 2014 at 6:21 Comment(2)
do you want to to apply same set of setters on two different conditions? correct me I see it differently. changing style will not help as it will be reverted as soon as it gets changed. lastly you can have data trigger to change the style of an element in the style of parent element of the desired element.Cermet
@pushpraj, yes, same setters. I'll try updatingJunette
C
23

Here is an example of how you may change the style of an element based on the data trigger:

<StackPanel>
    <Control Focusable="False">
        <Control.Template>
            <ControlTemplate>
                <!--resources-->
                <ControlTemplate.Resources>
                    <Style TargetType="Button" x:Key="primary">
                        <Setter Property="Content" Value="Primary style"/>
                    </Style>
                    <Style TargetType="Button" x:Key="secondary">
                        <Setter Property="Content" Value="Secondary style"/>
                    </Style>
                </ControlTemplate.Resources>
                <!--content-->
                <Button Style="{StaticResource primary}" x:Name="button"/>
                <!--triggers-->
                <ControlTemplate.Triggers>
                    <DataTrigger Binding="{Binding IsMouseOver,RelativeSource={RelativeSource Self}}" Value="true">
                        <Setter TargetName="button" Property="Style" Value="{StaticResource secondary}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding IsKeyboardFocusWithin,RelativeSource={RelativeSource Self}}" Value="true">
                        <Setter TargetName="button" Property="Style" Value="{StaticResource secondary}"/>
                    </DataTrigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Control.Template>
    </Control>
    <Button Content="A normal button"/>
</StackPanel>

In this example, Control is a wrapper element which will host the desired element in its control template. It is required to be in the control template if you wish to access it by name. A set of data triggers are defined in the control template's trigger, which will apply the style on the desired element (button) as needed.

I did not find a way to avoid duplicate setters. Perhaps the ability to swap/apply the style may help you achieve the same result.

If you do not wish to go for the control template approach, you may make use of attached properties or an unused Tag property in the element. In this example, we will take advantage of two way binding to achieve the same result.

Example:

<StackPanel>
    <Grid>
        <!--content-->
        <Button Style="{Binding Tag,RelativeSource={RelativeSource FindAncestor,AncestorType=Grid}}"/>
        <Grid.Style>
            <Style TargetType="Grid">
                <!--resources-->
                <Style.Resources>
                    <Style TargetType="Button" x:Key="primary">
                        <Setter Property="Content" Value="Primary style"/>
                    </Style>
                    <Style TargetType="Button" x:Key="secondary">
                        <Setter Property="Content" Value="Secondary style"/>
                    </Style>
                </Style.Resources>
                <Setter Property="Tag" Value="{StaticResource primary}"/>
                <!--triggers-->
                <Style.Triggers>
                    <DataTrigger Binding="{Binding IsMouseOver,RelativeSource={RelativeSource Self}}" Value="true">
                        <Setter Property="Tag" Value="{StaticResource secondary}"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding IsKeyboardFocusWithin,RelativeSource={RelativeSource Self}}" Value="true">
                        <Setter Property="Tag" Value="{StaticResource secondary}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Grid.Style>
    </Grid>
    <Button Content="A normal button"/>
</StackPanel>

Try it and see if this helps you to achieve the desired result.

Cermet answered 10/9, 2014 at 7:27 Comment(2)
A reluctant upvote. Both of those solutions seem pretty ugly, though I know that's not your fault :(Junette
There is a way to avoid multiple setters by using a binding to a multi-value converter. I'm not sure though that it's worth it, the syntax becomes very verbose :-/Biquadratic
H
2

Fast forward to 2024 I suffered the same situation when using materialDesignInXAML and want to change the style of a button from MaterialDesignRaisedDarkButton to MaterialDesignRaisedSecondaryDarkButton when the VM property changes.

To change the Style on DataTrigger you can use a MultiValue Converter to pass the data and the framework element to it and return a existing style:

XAML:

<UserControl.Resources>
    <converter:StyleSelectorConverter x:Key="StyleSelectorConverter" />
</UserControl.Resources>

....

<Button Content="Click here">
    <Button.Style>
        <MultiBinding Converter="{StaticResource StyleSelectorConverter}">
            <Binding Path="State" />
            <Binding RelativeSource="{RelativeSource Self}"/>
        </MultiBinding>
    </Button.Style>
</Button>

Converter:

public class StyleSelectorConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        return values.Length == 2 &&
               int.TryParse(values[0].ToString(), out var state) &&
               values[1] is FrameworkElement element ?
               state switch
               {
                   1 => (Style)element.FindResource("MaterialDesignRaisedSecondaryDarkButton"),
                   _ => (Style)element.FindResource("MaterialDesignRaisedDarkButton"),
               } : DependencyProperty.UnsetValue;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Be aware that this solution raises an exception when the desired resource is not found!

Honourable answered 23/7, 2024 at 14:27 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.