Data Trigger infinite looping and not as expected

I'm new to Triggers, so I'm probably missing something.

I have a TextToggleButton class that switches styles and visual states (using the Visual State Manager) when clicked.

I want to put multiple TextToggleButtons in a radio group. Rather than do it via code, I thought I'd give DataTriggers a try.

If there are only two buttons, I'm getting unexpected behavior. When I click "button1a" then click "button2a", "button1a" turns off as expected. When I click "button2a" again (turning it off), then "button1a" turns on which is not as expected. The trigger should only fire when IsToggled = true.

button1a Clicked
button1a OnIsToggledChanged(), IsToggled = True
button2a Clicked
button2a OnIsToggledChanged(), IsToggled = True
button1a OnIsToggledChanged(), IsToggled = False
button2a Clicked
button2a OnIsToggledChanged(), IsToggled = False
button1a OnIsToggledChanged(), IsToggled = True

If there four buttons, then I get an infinite trigger loop after pressing the third button

button1 Clicked
button1 OnIsToggledChanged(), IsToggled = True
button2 Clicked
button2 OnIsToggledChanged(), IsToggled = True
button1 OnIsToggledChanged(), IsToggled = False
button3 Clicked
button3 OnIsToggledChanged(), IsToggled = True
button2 OnIsToggledChanged(), IsToggled = False
button1 OnIsToggledChanged(), IsToggled = True
button3 OnIsToggledChanged(), IsToggled = False
button2 OnIsToggledChanged(), IsToggled = True
button1 OnIsToggledChanged(), IsToggled = False
then this repeats forever and I have to terminate the program
button3 OnIsToggledChanged(), IsToggled = True
button2 OnIsToggledChanged(), IsToggled = False
button1 OnIsToggledChanged(), IsToggled = True
button3 OnIsToggledChanged(), IsToggled = False
button2 OnIsToggledChanged(), IsToggled = True
button1 OnIsToggledChanged(), IsToggled = False

Here's my TextToggleButton class

public class TextToggleButton : Button
{
    public static BindableProperty IsToggledProperty =
        BindableProperty.Create("IsToggled", typeof(bool), typeof(TextToggleButton), false,
            propertyChanged: OnIsToggledChanged);

    public bool IsToggled
    {
        get => (bool)GetValue(IsToggledProperty);
        set => SetValue(IsToggledProperty, value);
    }

    public static BindableProperty ToggledOnStyleProperty =
        BindableProperty.Create("ToggledOnStyle", typeof(Style), typeof(TextToggleButton), null,
            propertyChanged: OnToggledOnStyleChanged);

    public Style ToggledOnStyle
    {
        get => (Style)GetValue(ToggledOnStyleProperty);
        set => SetValue(ToggledOnStyleProperty, value);
    }

    public static BindableProperty ToggledOffStyleProperty =
        BindableProperty.Create("ToggledOffStyle", typeof(Style), typeof(TextToggleButton), null,
            propertyChanged: OnToggledOffStyleChanged);

    public Style ToggledOffStyle
    {
        get => (Style)GetValue(ToggledOffStyleProperty);
        set => SetValue(ToggledOffStyleProperty, value);
    }

    public VisualStateGroup Vsg;
    public VisualState VsOn;
    public VisualState VsOff;

    public TextToggleButton()
    {
        Clicked += (sender, args) =>
        {
            Debug.WriteLine(((TextToggleButton) sender).Text + " Clicked");
            IsToggled ^= true;
        };
        Vsg = new VisualStateGroup { Name = "ToggleStates" };
    }

    protected override void OnParentSet()
    {
        base.OnParentSet();
        VisualStateManager.GoToState(this, "ToggledOff");
    }

    static void OnIsToggledChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var toggleButton = (TextToggleButton)bindable;
        var isToggled = (bool)newValue;

        Debug.WriteLine(toggleButton.Text + " OnIsToggledChanged(), IsToggled = " + toggleButton.IsToggled);

        VisualStateManager.GoToState(toggleButton, isToggled ? "ToggledOn" : "ToggledOff");
    }

    static void OnToggledOnStyleChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var toggleButton = (TextToggleButton)bindable;
        toggleButton.ToggledOnStyle = (Style)newValue;
        if ((Style) newValue == null) return;
        toggleButton.VsOn = new VisualState { Name = "ToggledOn" };
        toggleButton.VsOn.Setters.Add(new Setter
        {
            Property = StyleProperty,
            Value = (Style)newValue
        });
        toggleButton.Vsg.States.Add(toggleButton.VsOn);
        if (toggleButton.VsOn == null || toggleButton.VsOff == null) return;
        VisualStateManager.GetVisualStateGroups(toggleButton).Add(toggleButton.Vsg);
    }

    static void OnToggledOffStyleChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var toggleButton = (TextToggleButton)bindable;
        toggleButton.ToggledOffStyle = (Style) newValue;
        if ((Style)newValue == null) return;
        toggleButton.VsOff = new VisualState { Name = "ToggledOff" };
        toggleButton.VsOff.Setters.Add(new Setter
        {
            Property = StyleProperty,
            Value = (Style)newValue
        });
        toggleButton.Vsg.States.Add(toggleButton.VsOff);
        if (toggleButton.VsOn == null || toggleButton.VsOff == null) return;
        VisualStateManager.GetVisualStateGroups(toggleButton).Add(toggleButton.Vsg);
    }
}

and here's my xaml

<Grid ColumnSpacing="15"
      AbsoluteLayout.LayoutBounds="0.5,0.36,0.9,0.07"
      AbsoluteLayout.LayoutFlags="All">

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <controls:TextToggleButton x:Name="button1a" 
                               Grid.Column="0" 
                               Text="button1a" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button2a}, Path=IsToggled}"
                         Value="True">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

    <controls:TextToggleButton x:Name="button2a"
                               Grid.Column="1"
                               Text="button2a" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button1a}, Path=IsToggled}"
                         Value="True">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

</Grid>

<Grid ColumnSpacing="15"
      RowSpacing="15"
      AbsoluteLayout.LayoutBounds="0.5,0.6,0.9,0.16"
      AbsoluteLayout.LayoutFlags="All">

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>

    <controls:TextToggleButton x:Name="button1"
                               Grid.Row="0" Grid.Column="0" 
                               Text="button1" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button2}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button3}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button4}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

    <controls:TextToggleButton x:Name="button2"
                               Grid.Row="0" Grid.Column="1"
                               Text="button2" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button1}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button3}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button4}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

    <controls:TextToggleButton x:Name="button3"
                               Grid.Row="1" Grid.Column="0" 
                               Text="button3" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button1}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button2}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button4}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

    <controls:TextToggleButton x:Name="button4"
                               Grid.Row="1" Grid.Column="1"
                               Text="button4" 
                               BorderColor="{StaticResource HighlightColor}"
                               CornerRadius="10"
                               ToggledOnStyle="{StaticResource ToggleOnState}"
                               ToggledOffStyle="{StaticResource ToggleOffState}">
        <controls:TextToggleButton.Triggers>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button1}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button2}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
            <DataTrigger TargetType="controls:TextToggleButton"
                         Binding="{Binding Source={x:Reference button3}, Path=IsToggled}"
                         Value="true">
                <Setter Property="IsToggled" Value="False" />
            </DataTrigger>
        </controls:TextToggleButton.Triggers>
    </controls:TextToggleButton>

</Grid>

and here are my Styles

<Style x:Key="ToggleOnState" TargetType="controls:TextToggleButton">
    <Setter Property="BackgroundColor" Value="{StaticResource HighlightColor}" />
    <Setter Property="TextColor" Value="{StaticResource WhiteColor}" />
</Style>

<Style x:Key="ToggleOffState" TargetType="controls:TextToggleButton">
    <Setter Property="BackgroundColor" Value="{StaticResource WhiteColor}" />
    <Setter Property="TextColor" Value="{StaticResource HighlightColor}" />
</Style>

Answers

  • CaseCase USMember ✭✭✭

    Ah, I think I understand. The DataTrigger fires when all conditions are true and does the setter. The setter isn't really applied but is more like a mask. So when the conditions are false, the mask is removed, and the original value is re-set. There is an implied "else" to it.

    if (condition true) then
        apply setter mask covering IsToggled = true with IsToggled = false
    else
        remove mask returning IsToggled = true
    
Sign In or Register to comment.