4

I have created a bool to color like this:

public class BoolToColorConverter : BindableObject, IValueConverter
{
    public BoolToColorConverter()
    {
    }

    public static readonly BindableProperty TrueColorProperty =
        BindableProperty.Create(nameof(TrueColor), typeof(Color), typeof(BoolToColorConverter), null, BindingMode.OneWay, null, null);
  
    public Color TrueColor
    {
        get { return (Color) GetValue(TrueColorProperty); }
        set { SetValue(TrueColorProperty, value); }
    }
    
    public Color FalseColor { get; set; } = null!;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool b && b)
        {
            return TrueColor!;
        }

        return FalseColor!;
    }

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

With this I could use AppThemeBinding when I create the converter:

<ContentPage.Resources>
    <converts:BoolToColorConverter
        x:Key="ColorConverter" 
        TrueColor="{AppThemeBinding Light=DarkRed, Dark=LightSalmon}" 
        FalseColor="#888" />
</ContentPage.Resources>

<VerticalStackLayout>
    <Label
        Text="Hello, World!"
        TextColor="{Binding Source={x:Reference Checky}, Path=IsChecked, Converter={StaticResource ColorConverter}}"
        FontSize="32"
        HorizontalOptions="Center" />

    <CheckBox x:Name="Checky" />
</VerticalStackLayout>

This works as expected if the theme is set on start up.

Dark on start:

enter image description here

Ligh on start:

enter image description here

But if the theme is changed when the application is running, the binding is not reevaluated and the old color is shown. Here is how looks like if thmese is changed from dark mode to light:

enter image description here

Is there some workaround for this?

PEK
  • 3,688
  • 2
  • 31
  • 49
  • Converter will only run when the source bool, Checky, changes. Or if the containing layout or page is re-laid out. I’m not at pc, but maybe you can find how to tell page to lay out again. – ToolmakerSteve Jul 30 '22 at 21:05
  • I have tried to navigate to a new page and then back, but that didn't help. – PEK Jul 31 '22 at 12:58
  • Try [InvalidateArrange](https://stackoverflow.com/a/72327257/199364). Replace VLayout with the name of your layout. E.g. ``. – ToolmakerSteve Jul 31 '22 at 18:45
  • Thanks @ToolmakerSteve, I tried it, but it didn't work. The converter wasn't executed after InvalidateArrange was called. – PEK Jul 31 '22 at 20:04
  • Hmm. That didn't work. And going to another page and back didn't work. Might be a Maui bug. To get it looked at, please make a public github repository, with just enough code to build and run and see the problem. (Remove anything from the project that is not relevant to this bug.) Open an issue at https://github.com/dotnet/maui/issues, and provide link to your repo. Also add link here to the new issue, and vice versa. – ToolmakerSteve Aug 01 '22 at 00:07
  • @ToolmakerSteve, I think MAUI doing the right thing in my sample. The converter should only be executed when the bound value has been changed. Maybe it’s possible to get it to work with an AppThemeBinding object was returned, if that possible. Either way, I have solved it the Visual State Manager, see my answer below. – PEK Aug 01 '22 at 19:30

1 Answers1

2

You could use the Visual State Manager instead.

With StateTrigger

This is the easiest solution for this specific scenario. Add this converter:

internal class InvertedBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        try
        {
            return !(bool)value;
        }
        catch
        {
            return false;
        }
    }

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

And then use this XAML:

<ContentPage.Resources>
    <converts:InvertedBoolConverter x:Key="InvertedBoolConverter" />
</ContentPage.Resources>

<VerticalStackLayout 
    x:Name="MainLayout"
    Spacing="25" 
    Padding="30,0">

    <Label
    Text="Hello, World!"
    FontSize="32"
    HorizontalOptions="Center">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="Checked state">
                <VisualState Name="Checked">
                    <VisualState.StateTriggers>
                        <StateTrigger 
                            IsActive="{Binding Source={x:Reference Checky}, Path=IsChecked}"  
                        />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="TextColor" Value="{AppThemeBinding Light=DarkRed, Dark=LightSalmon}" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="Not checked">
                    <VisualState.StateTriggers>
                        <StateTrigger 
                            IsActive="{Binding Source={x:Reference Checky}, Path=IsChecked, Converter={StaticResource InvertedBoolConverter}}"
                        />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="TextColor" Value="#888" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Label>

    <CheckBox x:Name="Checky" />
</VerticalStackLayout>

With custom trigger

In a more complex scenario, you might want to use a customer trigger instead. Sample:

public class BoolTrigger : StateTriggerBase
{
    public static readonly BindableProperty 
        ValueProperty = BindableProperty.Create(nameof(Value), typeof(object), typeof(BoolTrigger), null, propertyChanged: OnBindablePropertyChanged);

    public object Value
    {
        get => GetValue(ValueProperty);
        set => SetValue(ValueProperty, value);
    }

    public bool OnValue { get; set; }

    private static void OnBindablePropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var trigger = bindable as BoolTrigger;
        trigger.UpdateTrigger();
    }

    private void UpdateTrigger()
    {
        if (Value is not bool val)
        {
            return;
        }

        SetActive(OnValue == val);
    }
}

And then like this in XAML:

<VerticalStackLayout 
    x:Name="MainLayout"
    Spacing="25" 
    Padding="30,0">

    <Label
    Text="Hello, World!"
    FontSize="32"
    HorizontalOptions="Center">
        <VisualStateManager.VisualStateGroups>
            <VisualStateGroup Name="Checked state">
                <VisualState Name="Checked">
                    <VisualState.StateTriggers>
                        <converts:BoolTrigger 
                            OnValue="true" 
                            Value="{Binding Source={x:Reference Checky}, Path=IsChecked}"  
                        />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="TextColor" Value="{AppThemeBinding Light=DarkRed, Dark=LightSalmon}" />
                    </VisualState.Setters>
                </VisualState>
                <VisualState Name="Not checked">
                    <VisualState.StateTriggers>
                        <converts:BoolTrigger 
                            OnValue="false" 
                            Value="{Binding Source={x:Reference Checky}, Path=IsChecked}"  
                        />
                    </VisualState.StateTriggers>
                    <VisualState.Setters>
                        <Setter Property="TextColor" Value="#888" />
                    </VisualState.Setters>
                </VisualState>
            </VisualStateGroup>
        </VisualStateManager.VisualStateGroups>
    </Label>

    <CheckBox x:Name="Checky" />
</VerticalStackLayout>
PEK
  • 3,688
  • 2
  • 31
  • 49
  • Glad you found a solution. FYI, Its surprising that this works, but the original approach did not. These state triggers should fire in the exact same situations as the bindings should be re-evaluated! (So it still looks to me like the original failure is a Maui bug.) – ToolmakerSteve Aug 01 '22 at 19:34
  • @ToolmakerSteve, yes, are are triggered in the same situations. But in my original attempt a fixed color was returned, but the Visual State Manager a AppThemeObject are returned. – PEK Aug 01 '22 at 20:18
  • I see. When declaring converter, that theme binding apparently only gets evaluated once, when converter is created. I would not have guessed that. – ToolmakerSteve Aug 01 '22 at 20:30
  • The binding is not evaluated when the converter is created, it is evaluated in the `Convert` method which is not called when the theme is changed. If I changed the theme and then unchecked and then checked the right color would have been used. – PEK Aug 02 '22 at 06:17