AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.35k stars 2.2k forks source link

ItemsControl.ItemContainerTheme doesn't have new value applied after change #12620

Closed billhenn closed 1 year ago

billhenn commented 1 year ago

Describe the bug The ItemsControl.ItemContainerTheme property is important because it allows us to define how the containers of items should appear in relation to the ControlTheme for the ItemsControl itself.

There is an ItemsControl bug right now where if you use a container control that would have been generated as the "item" itself, any future changes to ItemContainerTheme are not reapplied to existing containers.

To Reproduce Use this code in an app and click the button at the bottom. The button will change the TabControl theme, which should in turn also update the TabItem theme used. But that doesn't happen.

XAML:

<StackPanel>
    <StackPanel.Resources>

        <ControlTheme x:Key="BaseTabItemControlTheme" TargetType="TabItem">
            <Setter Property="Padding" Value="5" />
            <Setter Property="Template">
                <ControlTemplate>

                    <Border
                        x:Name="PART_LayoutRoot"
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"
                        Padding="{TemplateBinding Padding}">
                        <ContentPresenter x:Name="PART_ContentPresenter" Content="{TemplateBinding Header}" ContentTemplate="{TemplateBinding HeaderTemplate}" />
                    </Border>

                </ControlTemplate>
            </Setter>
        </ControlTheme>

        <ControlTheme x:Key="FirstTabItemControlTheme" TargetType="TabItem" BasedOn="{StaticResource BaseTabItemControlTheme}">
            <Setter Property="Background" Value="LightSalmon" />
            <Setter Property="HeaderTemplate">
                <DataTemplate>
                    <TextBlock Margin="10" Text="{ReflectionBinding StringFormat='{}{0} - First theme header'}" />
                </DataTemplate>
            </Setter>
            <Setter Property="ContentTemplate">
                <DataTemplate>
                    <TextBlock Margin="10" Text="{ReflectionBinding StringFormat='{}{0} - First theme content'}" />
                </DataTemplate>
            </Setter>
        </ControlTheme>

        <ControlTheme x:Key="SecondTabItemControlTheme" TargetType="TabItem" BasedOn="{StaticResource BaseTabItemControlTheme}">
            <Setter Property="Background" Value="LightGreen" />
            <Setter Property="HeaderTemplate">
                <DataTemplate>
                    <TextBlock Margin="10" Text="{ReflectionBinding StringFormat='{}{0} - Second theme header'}" />
                </DataTemplate>
            </Setter>
            <Setter Property="ContentTemplate">
                <DataTemplate>
                    <TextBlock Margin="10" Text="{ReflectionBinding StringFormat='{}{0} - Second theme content'}" />
                </DataTemplate>
            </Setter>
        </ControlTheme>

        <ControlTheme x:Key="BaseTabControlTheme" TargetType="TabControl">
            <Setter Property="ItemsPanel">
                <ItemsPanelTemplate>
                    <StackPanel Orientation="Horizontal" />
                </ItemsPanelTemplate>
            </Setter>
            <Setter Property="Padding" Value="20" />
            <Setter Property="Template">
                <ControlTemplate>

                    <DockPanel>
                        <ItemsPresenter x:Name="PART_ItemsPresenter" DockPanel.Dock="Top" ItemsPanel="{TemplateBinding ItemsPanel}" ZIndex="1" />

                        <Border
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="{TemplateBinding CornerRadius}">
                            <ContentPresenter
                                x:Name="PART_SelectedContentHost"
                                Margin="{TemplateBinding Padding}"
                                Content="{TemplateBinding SelectedContent}"
                                ContentTemplate="{TemplateBinding SelectedContentTemplate}"
                                HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                                VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
                        </Border>
                    </DockPanel>

                </ControlTemplate>
            </Setter>
        </ControlTheme>

        <ControlTheme x:Key="FirstTabControlTheme" TargetType="TabControl" BasedOn="{StaticResource BaseTabControlTheme}">
            <Setter Property="BorderBrush" Value="Red" />
            <Setter Property="ItemContainerTheme" Value="{StaticResource FirstTabItemControlTheme}" />
        </ControlTheme>

        <ControlTheme x:Key="SecondTabControlTheme" TargetType="TabControl" BasedOn="{StaticResource BaseTabControlTheme}">
            <Setter Property="BorderBrush" Value="Green" />
            <Setter Property="ItemContainerTheme" Value="{StaticResource SecondTabItemControlTheme}" />
        </ControlTheme>

        <ControlTheme x:Key="{x:Type TabControl}" TargetType="TabControl" BasedOn="{StaticResource FirstTabControlTheme}" />
    </StackPanel.Resources>

    <TabControl x:Name="tabControl" BorderThickness="3">
        <TabItem Header="Tab 1">Content 1</TabItem>
        <TabItem Header="Tab 2">Content 2</TabItem>
        <TabItem Header="Tab 3">Content 3</TabItem>
    </TabControl>
    <Button x:Name="toggleThemeButton" Content="Change to Second Theme" />
</StackPanel>

C#:

toggleThemeButton.Click += (sender, e) => {
    tabControl.Theme = tabControl.FindResource("SecondTabControlTheme") as ControlTheme;
};

Before clicking the button, you'll see this at startup: image

After you click the button, you'll see this, which as you can see does not update the item theme: image

Expected behavior When clicking the button in the code above, the UI should look like this instead: image

Cause The problem is this if condition in ItemsControl.PrepareItemContainer. When the control is first displayed, it assigns each container.Theme ok but then if we change the ItemContainerTheme later, while it does seem to re-execute this logic, the new container.Theme is never applied since that !container.IsSet(ThemeProperty) check fails.

If that !container.IsSet(ThemeProperty) line is commented out, the expected behavior is achieved. I'm not sure the ideal fix for this (taking that line out or something else), but we do need to get it working to support control themes properly.

Environment Windows 11 running Avalonia v11.0.4.

timunie commented 1 year ago

probably not using IsSet but checking if it's equal to the set one? Something like this:

  if (itemContainerTheme is not null &&
      !container.IsSet(ThemeProperty) &&
+     itemContainerTheme != container.Theme &&
      StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType)

can you test this in your sample?

billhenn commented 1 year ago

can you test this in your sample?

@timunie Sure... from testing, if we pull out and replace the IsSet line with the one you suggested, it works for us.

if (itemContainerTheme is not null &&
    itemContainerTheme != container.Theme &&
    StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType)