Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Too many ClipBorder controls cause rendering bugs #4524

Open
RayCarrot opened this issue Oct 29, 2024 · 5 comments
Open

Too many ClipBorder controls cause rendering bugs #4524

RayCarrot opened this issue Oct 29, 2024 · 5 comments
Labels

Comments

@RayCarrot
Copy link

Describe the bug

I use the ClipBorder control in various control styles in my app to allow for rounded corners. However after doing this I noticed that some of the controls which use it start experiencing rendering issues (rounded corners not applying, background color not changing when it should etc.) when there are too many of them in total.

After a lot of experimenting I narrowed it down to the ClipBorder control causing it if there are too many in use at once. It appears to be specifically this line of code causing it, so it's somehow related to the clip being set.

Steps to reproduce

The issue can easily be reproduced with this XAML:

<ScrollViewer>
    <ItemsControl ItemsSource="{Binding Source={x:Static local:MainWindow.Items}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <mah:ClipBorder CornerRadius="20" Width="64" Height="64" Margin="2">
                    <Border Background="Red">
                        <TextBlock Text="{Binding}" 
                                   VerticalAlignment="Center" 
                                   HorizontalAlignment="Center" />
                    </Border>
                </mah:ClipBorder>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</ScrollViewer>

and then a static property like this for the items to bind to:

public static int[] Items => Enumerable.Range(0, 1000).ToArray();

This will display a big list of red squares which should be rounded. However as you will notice it breaks after rendering 145 of them.
ClipBorderBug_OpKKflcSm5

It's not only the rounded corners that break, but other things too. For example say we made it so the color of the squares should change from red to green on mouse over:

<ScrollViewer>
    <ItemsControl ItemsSource="{Binding Source={x:Static local:MainWindow.Items}}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <mah:ClipBorder CornerRadius="20" Width="64" Height="64" Margin="2">
                    <Border>
                        <Border.Style>
                            <Style TargetType="{x:Type Border}">
                                <Setter Property="Background" Value="Red" />
                                <Style.Triggers>
                                    <Trigger Property="IsMouseOver" Value="True">
                                        <Setter Property="Background" Value="Green" />
                                    </Trigger>
                                </Style.Triggers>
                            </Style>
                        </Border.Style>
                        <TextBlock Text="{Binding}"
                                   VerticalAlignment="Center" 
                                   HorizontalAlignment="Center" />
                    </Border>
                </mah:ClipBorder>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
        <ItemsControl.ItemsPanel>
            <ItemsPanelTemplate>
                <WrapPanel />
            </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
    </ItemsControl>
</ScrollViewer>

Now this works on all squares up to 145 where this also breaks.

Environment

MahApps.Metro version: v2.4.10
Windows build number: Win11 23H2
Visual Studio: 2022 17.11.5
Target Framework: .NET Framework 4.7.2
@RayCarrot RayCarrot added the Bug label Oct 29, 2024
@punker76
Copy link
Member

Hey @RayCarrot

the ClipBorder, yes, it's a special control, maybe it will gone sometimes. But to help you, here some solution hints.

  • first: never put a ScrollViewer around an ItemsControl (ListBox, ListView...) It breaks always virtualization
  • sec: use an ItemsControl with virtualization and ScrollViewer inside and change the DataTemplate a little bit
<Grid>
	<Grid.Resources>
		<Style x:Key="ScrollBarItemsControlStyle" TargetType="{x:Type ItemsControl}">
			<Setter Property="BorderThickness" Value="0" />
			<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
			<Setter Property="VirtualizingPanel.IsVirtualizingWhenGrouping" Value="True" />
			<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
			<Setter Property="Template">
				<Setter.Value>
					<ControlTemplate TargetType="{x:Type ItemsControl}">
						<Border x:Name="Border"
								Margin="0"
								SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
								Background="{TemplateBinding Background}"
								BorderBrush="{TemplateBinding BorderBrush}"
								BorderThickness="{TemplateBinding BorderThickness}">
							<ScrollViewer Margin="0"
										  Focusable="False"
										  Padding="{TemplateBinding Padding}"
										  SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
										  HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
										  VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
										  CanContentScroll="{TemplateBinding ScrollViewer.CanContentScroll}">
								<ItemsPresenter SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
							</ScrollViewer>
						</Border>
					</ControlTemplate>
				</Setter.Value>
			</Setter>
		</Style>
	</Grid.Resources>

	<ItemsControl ItemsSource="{Binding Source={x:Static local:MainWindow.Items}}"
				  Style="{StaticResource ScrollBarItemsControlStyle}"
				  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
				  ScrollViewer.VerticalScrollBarVisibility="Auto"
				  VirtualizingPanel.CacheLengthUnit="Item"
				  VirtualizingPanel.ScrollUnit="Item"
				  VirtualizingPanel.VirtualizationMode="Recycling">
		<ItemsControl.ItemTemplate>
			<DataTemplate>
				<ContentControl Content="{Binding}">
					<ContentControl.Template>
						<ControlTemplate>
							<Grid Width="64" Height="64">
								<mah:ClipBorder Background="Red" x:Name="Back" CornerRadius="20" Margin="2">
									<Grid Background="Transparent">
										<TextBlock Text="{Binding}"
												   VerticalAlignment="Center"
												   HorizontalAlignment="Center" />
									</Grid>
								</mah:ClipBorder>
							</Grid>
							<ControlTemplate.Triggers>
								<Trigger Property="IsMouseOver" Value="True">
									<Setter TargetName="Back" Property="Background" Value="Green" />
								</Trigger>
							</ControlTemplate.Triggers>
						</ControlTemplate>
					</ContentControl.Template>
				</ContentControl>
			</DataTemplate>
		</ItemsControl.ItemTemplate>
		<ItemsControl.ItemsPanel>
			<ItemsPanelTemplate>
				<WrapPanel />
			</ItemsPanelTemplate>
		</ItemsControl.ItemsPanel>
	</ItemsControl>

</Grid>
<ItemsControl.ItemsPanel>
	<ItemsPanelTemplate>
		<controls:VirtualizingWrapPanel IsItemsHost="True"
										ItemSize="64,64"
										MouseWheelDelta="1"
										Orientation="Horizontal"
										ScrollLineDeltaItem="1"
										MouseWheelDeltaItem="1"
										SpacingMode="None"
										StretchItems="False" />
	</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
  • and last: try not using ClipBorder :-D
                            <ControlTemplate>
                                <Grid Width="64" Height="64">
                                    <Border Background="Red" x:Name="Back" CornerRadius="20" Margin="2">
                                        <Grid Background="Transparent">
                                            <TextBlock Text="{Binding}"
                                                       VerticalAlignment="Center"
                                                       HorizontalAlignment="Center" />
                                        </Grid>
                                    </Border>
                                </Grid>
                                <ControlTemplate.Triggers>
                                    <Trigger Property="IsMouseOver" Value="True">
                                        <Setter TargetName="Back" Property="Background" Value="Green" />
                                    </Trigger>
                                </ControlTemplate.Triggers>
                            </ControlTemplate>

2024-10-30_00h12_49

@RayCarrot
Copy link
Author

Thanks for the quick reply! I do want to clarify that the code I posted is just an example to easily be able to reproduce the issue. The ClipBorder can be helpful over setting the corner radius of a normal Border since it also clips the contents, like if there's an image inside or some other content that shouldn't leak out. That's why I use it in several ControlTemplates just like how it is in this library.

Although I don't display 1000 items in a list like in this example it is true that there are some lists which I do not virtualize, mainly because of how they're sized or grouped. I suppose I could look into improving that. Thanks for linking the VirtualizedWrapPanel control! I didn't know such a thing existed - that will be very helpful!

So yeah, I was hoping the issue could be fixed directly in the ClipBorder, but if it's more of a limitation with how the clipping works in large numbers in WPF then feel free to close the issue and I'll look into restructuring some things in my app and relying on virtualization more.

@punker76
Copy link
Member

"So yeah, I was hoping the issue could be fixed directly in the ClipBorder, but if it's more of a limitation with how the clipping works in large numbers in WPF then feel free to close the issue and I'll look into restructuring some things in my app and relying on virtualization more."

Fir this I must go deeper in ClipBorder. Fun fact is, if I use Button with the default MahApps style which has ClipBorder inside, then there is no issue 🙄

@punker76
Copy link
Member

punker76 commented Oct 30, 2024

@RayCarrot So, after some investigation, the problem is really to set the clip inside the ArrangeOverride method. I have tried to get another solution, doing the clip stuff a little bit different, but with the same result and now it works also without the virtualization.

I use now this converter, to generate the clip for the inner grid root element

public class ClipGeometryConverter : IMultiValueConverter
{
	/// <inheritdoc />
	public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
	{
		if (values.Length != 5)
		{
			return DependencyProperty.UnsetValue;
		}

		if (!(values[0] is double width)
			|| !(values[1] is double height)
			|| !(values[2] is CornerRadius cornerRadius)
			|| !(values[3] is Thickness borderThickness)
			|| !(values[4] is Thickness padding))
		{
			return DependencyProperty.UnsetValue;
		}

		if (width <= 0 || height <= 0)
		{
			return DependencyProperty.UnsetValue;
		}

		var clipGeometry = new StreamGeometry();
		var childBorderInfo = new ClipBorder.BorderInfo(cornerRadius, borderThickness, padding, false);
		using (var ctx = clipGeometry.Open())
		{
			ClipBorder.GenerateGeometry(ctx, new Rect(0, 0, width, height), childBorderInfo);
		}

		// Freeze the geometry for better perfomance
		clipGeometry.Freeze();
		return clipGeometry;
	}

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

Then I use the converter in that way

<ItemsControl Margin="10" ItemsSource="{Binding Source={x:Static local:MainWindow.Items}}"
			  Style="{StaticResource ScrollBarItemsControlStyle}"
			  ScrollViewer.HorizontalScrollBarVisibility="Disabled"
			  ScrollViewer.VerticalScrollBarVisibility="Auto"
			  VirtualizingPanel.CacheLengthUnit="Item"
			  VirtualizingPanel.ScrollUnit="Item"
			  VirtualizingPanel.VirtualizationMode="Recycling">
	<ItemsControl.Resources>
		<local:ClipGeometryConverter x:Key="ClipGeometryConverter" />
	</ItemsControl.Resources>
	<ItemsControl.ItemTemplate>
		<DataTemplate>
			<myControls:ClipBorder x:Name="Border"
								   BorderThickness="2"
								   BorderBrush="Black"
								   CornerRadius="20"
								   Margin="2">
				<Border.Style>
					<Style TargetType="{x:Type Border}">
						<Setter Property="Background" Value="Red" />
						<Style.Triggers>
							<Trigger Property="IsMouseOver" Value="True">
								<Setter Property="Background" Value="Green" />
							</Trigger>
						</Style.Triggers>
					</Style>
				</Border.Style>

				<Grid x:Name="Root">
					<Grid.Clip>
						<MultiBinding Converter="{StaticResource ClipGeometryConverter}">
							<Binding ElementName="Root" Path="ActualWidth" Mode="OneWay" />
							<Binding ElementName="Root" Path="ActualHeight" Mode="OneWay" />
							<Binding ElementName="Border" Path="CornerRadius" Mode="OneWay" />
							<Binding ElementName="Border" Path="BorderThickness" Mode="OneWay" />
							<Binding ElementName="Border" Path="Padding" Mode="OneWay" />
						</MultiBinding>
					</Grid.Clip>
					<TextBlock Text="{Binding}"
							   VerticalAlignment="Center"
							   HorizontalAlignment="Center" />
				</Grid>
			</myControls:ClipBorder>
		</DataTemplate>
	</ItemsControl.ItemTemplate>
	<ItemsControl.ItemsPanel>
		<ItemsPanelTemplate>
			<!-- <WrapPanel /> -->
			<controls:VirtualizingWrapPanel IsItemsHost="True"
											ItemSize="64,64"
											MouseWheelDelta="1"
											Orientation="Horizontal"
											ScrollLineDeltaItem="1"
											MouseWheelDeltaItem="1"
											SpacingMode="None"
											StretchItems="False" />
		</ItemsPanelTemplate>
	</ItemsControl.ItemsPanel>
</ItemsControl>

Here is the complete project with the changed ClipBorder: WpfApp3.zip

It is not so elegant/easy/whatever but has no side effect with many controls. Maybe I can use this in MahApps as default.

@RayCarrot
Copy link
Author

Thank you so much That converter seems to work really well 🙂 No more rendering issues as far as I can tell! Seeing this reminded me of a similar converter in the Material Design in XAML repo which I remember seeing before. Would be awesome if this could be integrated into the MahApps.Metro library at some point since this approach seems to cause fewer issues.

RayCarrot added a commit to RayCarrot/RayCarrot.RCP.Metro that referenced this issue Oct 31, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Development

No branches or pull requests

2 participants