Benjol
Benjol

Reputation: 66531

Changing style from inside a DataTrigger

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?

Upvotes: 17

Views: 16271

Answers (2)

Daimonion
Daimonion

Reputation: 156

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!

Upvotes: 3

pushpraj
pushpraj

Reputation: 13669

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.

Upvotes: 24

Related Questions